agent-device 0.2.0 → 0.2.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 +10 -10
- package/dist/src/daemon.js +1 -1
- package/package.json +1 -1
- package/skills/agent-device/SKILL.md +3 -4
- package/skills/agent-device/references/permissions.md +3 -3
- package/skills/agent-device/references/session-management.md +1 -1
- package/skills/agent-device/references/snapshot-refs.md +5 -5
- package/src/platforms/ios/runner-client.ts +1 -1
package/README.md
CHANGED
|
@@ -26,12 +26,12 @@ npx agent-device open SampleApp
|
|
|
26
26
|
## Quick Start
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
agent-device open Contacts --platform ios
|
|
30
|
-
agent-device snapshot
|
|
31
|
-
agent-device click @e5
|
|
32
|
-
agent-device fill @e6 "John"
|
|
33
|
-
agent-device fill @e7 "Doe"
|
|
34
|
-
agent-device click @e3
|
|
29
|
+
agent-device open Contacts --platform ios # creates session on iOS Simulator
|
|
30
|
+
agent-device snapshot
|
|
31
|
+
agent-device click @e5
|
|
32
|
+
agent-device fill @e6 "John"
|
|
33
|
+
agent-device fill @e7 "Doe"
|
|
34
|
+
agent-device click @e3
|
|
35
35
|
agent-device close
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -55,7 +55,7 @@ Debug flow:
|
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
agent-device trace start
|
|
58
|
-
agent-device snapshot
|
|
58
|
+
agent-device snapshot -s "Sample App"
|
|
59
59
|
agent-device find label "Wi-Fi" click
|
|
60
60
|
agent-device trace stop ./trace.log
|
|
61
61
|
```
|
|
@@ -77,8 +77,8 @@ Coordinates:
|
|
|
77
77
|
|
|
78
78
|
| Backend | Speed | Accuracy | Requirements |
|
|
79
79
|
| --- | --- | --- | --- |
|
|
80
|
-
| `ax` | Fast | Medium | Accessibility permission for the terminal app |
|
|
81
80
|
| `xctest` | Fast | High | No Accessibility permission required |
|
|
81
|
+
| `ax` | Fast | Medium | Accessibility permission for the terminal app, not recommended |
|
|
82
82
|
|
|
83
83
|
Notes:
|
|
84
84
|
- Default backend is `xctest` on iOS.
|
|
@@ -126,9 +126,9 @@ App state:
|
|
|
126
126
|
|
|
127
127
|
- `agent-device trace start`
|
|
128
128
|
- `agent-device trace stop ./trace.log`
|
|
129
|
-
- The trace log includes
|
|
129
|
+
- The trace log includes snapshot logs and XCTest runner logs for the session.
|
|
130
130
|
- Built-in retries cover transient runner connection failures, AX snapshot hiccups, and Android UI dumps.
|
|
131
|
-
- For snapshot issues, compare `--
|
|
131
|
+
- For snapshot issues (missing elements), compare with `--raw` flag for unaltered output and scope with `-s "<label>"`.
|
|
132
132
|
|
|
133
133
|
## App resolution
|
|
134
134
|
- Bundle/package identifiers are accepted directly (e.g., `com.apple.Preferences`).
|
package/dist/src/daemon.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
let e,t;import a from"node:crypto";import{isCancel as i,select as o}from"@clack/prompts";import{node_path as n,runCmdStreaming as r,promises as s,asAppError as l,fileURLToPath as c,runCmdBackground as u,node_fs as d,node_os as p,errors_AppError as f,runCmd as h,node_net as m,whichCmd as w}from"./861.js";async function g(e,t){let a=e,n=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(a=a.filter(e=>e.platform===t.platform)),t.udid){let e=a.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=a.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=n(t.deviceName),i=a.find(t=>n(t.name)===e);if(!i)throw new f("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===a.length)return a[0];if(0===a.length)throw new f("DEVICE_NOT_FOUND","No devices found",{selector:t});let r=a.filter(e=>e.booted);if(1===r.length)return r[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await o({message:"Multiple devices available. Choose a device to continue:",options:(r.length>0?r:a).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new f("INVALID_ARGS","Device selection cancelled");if(e){let t=a.find(t=>t.id===e);if(t)return t}}return r[0]??a[0]}async function v(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let a of e){if(!a||a.startsWith("List of devices"))continue;let e=a.split(/\s+/),i=e[0];if("device"!==e[1])continue;let o=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||i;if(i.startsWith("emulator-")){let e=await h("adb",["-s",i,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(o=t.replace(/_/g," "))}let n=await y(i);t.push({platform:"android",id:i,name:o,kind:i.startsWith("emulator-")?"emulator":"device",booted:n})}return t}async function y(e){try{let t=await h("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}async function I(e,t=6e4){let a=Date.now();for(;Date.now()-a<t;){if(await y(e))return;await new Promise(e=>setTimeout(e,1e3))}throw new f("COMMAND_FAILED","Android device did not finish booting in time",{serial:e,timeoutMs:t})}async function N(e,t={}){let a,i=t.attempts??3,o=t.baseDelayMs??200,n=t.maxDelayMs??2e3,r=t.jitter??.2;for(let s=1;s<=i;s+=1)try{return await e()}catch(l){if(a=l,s>=i||t.shouldRetry&&!t.shouldRetry(l,s))break;let e=function(e,t,a,i){let o=Math.min(t,e*2**(i-1));return Math.max(0,o+o*a*(2*Math.random()-1))}(o,n,r,s);await function(e){return new Promise(t=>setTimeout(t,e))}(e)}if(a)throw a;throw new f("COMMAND_FAILED","retry failed")}let A={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function S(e,t){let a=t.trim();if(a.includes("."))return{type:"package",value:a};let i=A[a.toLowerCase()];if(i)return i;let o=(await h("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(a.toLowerCase()));if(1===o.length)return{type:"package",value:o[0]};if(o.length>1)throw new f("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:o});throw new f("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function D(e,t="launchable"){if("launchable"===t){let t=await h("adb",b(e,["shell","cmd","package","query-activities","--brief","-a","android.intent.action.MAIN","-c","android.intent.category.LAUNCHER"]),{allowFailure:!0});if(0===t.exitCode&&t.stdout.trim().length>0){let e=new Set;for(let a of t.stdout.split("\n")){let t=a.trim();if(!t)continue;let i=t.split(/\s+/)[0],o=i.includes("/")?i.split("/")[0]:i;o&&e.add(o)}if(e.size>0)return Array.from(e)}}return(await h("adb",b(e,"user-installed"===t?["shell","pm","list","packages","-3"]:["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean)}async function k(e,t="launchable"){let a=await D(e,t),i=new Set("launchable"===t?a:await D(e,"launchable"));return a.map(e=>({package:e,launchable:i.has(e)}))}async function P(e){let t=await O(e,[["shell","dumpsys","window","windows"],["shell","dumpsys","window"]]);if(t)return t;let a=await O(e,[["shell","dumpsys","activity","activities"],["shell","dumpsys","activity"]]);return a||{}}async function O(e,t){for(let a of t){let t=function(e){for(let t of[/mCurrentFocus=Window\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mFocusedApp=AppWindowToken\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/,/ResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/]){let a=t.exec(e);if(a)return{package:a[1],activity:a[2]}}return null}((await h("adb",b(e,a),{allowFailure:!0})).stdout??"");if(t)return t}return null}async function x(e,t){e.booted||await I(e.id);let a=await S(e,t);"intent"===a.type?await h("adb",b(e,["shell","am","start","-a",a.value])):await h("adb",b(e,["shell","monkey","-p",a.value,"-c","android.intent.category.LAUNCHER","1"]))}async function _(e){e.booted||await I(e.id)}async function L(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",b(e,["shell","am","force-stop","com.android.settings"]));let a=await S(e,t);if("intent"===a.type)throw new f("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",b(e,["shell","am","force-stop",a.value]))}async function E(e,t,a){await h("adb",b(e,["shell","input","tap",String(t),String(a)]))}async function R(e){await h("adb",b(e,["shell","input","keyevent","4"]))}async function C(e){await h("adb",b(e,["shell","input","keyevent","3"]))}async function M(e){await h("adb",b(e,["shell","input","keyevent","187"]))}async function T(e,t,a,i=800){await h("adb",b(e,["shell","input","swipe",String(t),String(a),String(t),String(a),String(i)]))}async function F(e,t){let a=t.replace(/ /g,"%s");await h("adb",b(e,["shell","input","text",a]))}async function B(e,t,a){await E(e,t,a)}async function $(e,t,a,i){await B(e,t,a),await F(e,i)}async function U(e,t,a=.6){let{width:i,height:o}=await W(e),n=Math.floor(i*a),r=Math.floor(o*a),s=Math.floor(i/2),l=Math.floor(o/2),c=s,u=l,d=s,p=l;switch(t){case"up":u=l-Math.floor(r/2),p=l+Math.floor(r/2);break;case"down":u=l+Math.floor(r/2),p=l-Math.floor(r/2);break;case"left":c=s-Math.floor(n/2),d=s+Math.floor(n/2);break;case"right":c=s+Math.floor(n/2),d=s-Math.floor(n/2);break;default:throw new f("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",b(e,["shell","input","swipe",String(c),String(u),String(d),String(p),"300"]))}async function V(e,t){for(let a=0;a<8;a+=1){let a="";try{a=await X(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new f("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let a=t.toLowerCase(),i=/<node[^>]+>/g,o=i.exec(e);for(;o;){let t=o[0],n=/text="([^"]*)"/.exec(t),r=/content-desc="([^"]*)"/.exec(t),s=(n?.[1]??"").toLowerCase(),l=(r?.[1]??"").toLowerCase();if(s.includes(a)||l.includes(a)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),a=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((a+Number(e[4]))/2)}}return{x:0,y:0}}o=i.exec(e)}return null}(a,t))return;await U(e,"down",.5)}throw new f("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function j(e,t){let a=await h("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!a.stdoutBuffer)throw new f("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,a.stdoutBuffer)}async function G(e,t,a){let i=t.toLowerCase(),o=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(i){case"wifi":return void await h("adb",b(e,["shell","svc","wifi",o?"enable":"disable"]));case"airplane":await h("adb",b(e,["shell","settings","put","global","airplane_mode_on",o?"1":"0"])),await h("adb",b(e,["shell","am","broadcast","-a","android.intent.action.AIRPLANE_MODE","--ez","state",o?"true":"false"]));return;case"location":return void await h("adb",b(e,["shell","settings","put","secure","location_mode",o?"3":"0"]));default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}async function q(e,t={}){return function(e,t,a){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},a=[t],i=/<node\b[^>]*>|<\/node>/g,o=i.exec(e);for(;o;){let t=o[0];if(t.startsWith("</node")){a.length>1&&a.pop(),o=i.exec(e);continue}let n=function(e){let t=t=>{let a=RegExp(`${t}="([^"]*)"`).exec(e);return a?a[1]:null},a=e=>{let a=t(e);if(null!==a)return"true"===a};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:a("clickable"),enabled:a("enabled"),focusable:a("focusable")}}(t),r=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let a=Number(t[1]),i=Number(t[2]);return{x:a,y:i,width:Math.max(0,Number(t[3])-a),height:Math.max(0,Number(t[4])-i)}}(n.bounds),s=a[a.length-1],l={type:n.className,label:n.text||n.desc,value:n.text,identifier:n.resourceId,rect:r,enabled:n.enabled,hittable:n.clickable??n.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||a.push(l),o=i.exec(e)}return t}(e),o=[],n=!1,r=a.depth??1/0,s=a.scope?function(e,t){let a=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",o=e.value?.toLowerCase()??"",n=e.identifier?.toLowerCase()??"";if(t.includes(a)||o.includes(a)||n.includes(a))return e;i.push(...e.children)}return null}(i,a.scope):null,l=s?[s]:i.children,c=(e,t)=>{if(o.length>=800){n=!0;return}if(!(t>r)){for(let i of((a.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),a=!!(e.identifier&&e.identifier.trim().length>0);return t||a||!!e.hittable}return!0}(e,a))&&o.push({index:o.length,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:e.parentIndex}),e.children))if(c(i,t+1),n)return}};for(let e of l)if(c(e,0),n)break;return n?{nodes:o,truncated:n}:{nodes:o}}(await X(e),800,t)}async function J(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH")}async function W(e){let t=(await h("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new f("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function X(e){return N(()=>z(e),{shouldRetry:H})}async function z(e){return await h("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}function H(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.details?.stderr??""}`.toLowerCase();return!!(t.includes("device offline")||t.includes("device not found")||t.includes("transport error")||t.includes("connection reset")||t.includes("broken pipe")||t.includes("timed out"))}async function Z(){if("darwin"!==process.platform)throw new f("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await w("xcrun"))throw new f("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices))for(let a of t)a.isAvailable&&e.push({platform:"ios",id:a.udid,name:a.name,kind:"simulator",booted:"Booted"===a.state})}catch(e){throw new f("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await w("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let a of JSON.parse(t.stdout).devices??[])a.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:a.identifier,name:a.name,kind:"device",booted:!0})}catch{}return e}let Y={settings:"com.apple.Preferences"};async function K(e,t){let a=t.trim();if(a.includes("."))return a;let i=Y[a.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await ep(e)).filter(e=>e.name.toLowerCase()===a.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new f("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new f("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function Q(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}),await h("xcrun",["simctl","launch",e.id,a]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,a])}async function ee(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}))}async function et(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e);let t=await h("xcrun",["simctl","terminate",e.id,a],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new f("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,a],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,a])}async function ea(e,t,a){throw ed(e,"press"),new f("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function ei(e,t,a,i=800){throw ed(e,"long-press"),new f("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function eo(e,t,a){await ea(e,t,a)}async function en(e,t){throw ed(e,"type"),new f("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function er(e,t,a,i){await eo(e,t,a),await en(e,i)}async function es(e,t,a=.6){throw ed(e,"scroll"),new f("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function el(e){throw new f("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function ec(e,t){if("simulator"===e.kind){await ef(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}async function eu(e,t,a,i){ed(e,"settings"),await ef(e);let o=t.toLowerCase(),n=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(o){case"wifi":return void await h("xcrun",["simctl","status_bar",e.id,"override","--wifiMode",n?"active":"failed"]);case"airplane":n?await h("xcrun",["simctl","status_bar",e.id,"override","--dataNetwork","hide","--wifiMode","failed","--wifiBars","0","--cellularMode","failed","--cellularBars","0","--operatorName",""]):await h("xcrun",["simctl","status_bar",e.id,"clear"]);return;case"location":if(!i)throw new f("INVALID_ARGS","location setting requires an active app in session");await h("xcrun",["simctl","privacy",e.id,n?"grant":"revoke","location",i]);return;default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}function ed(e,t){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function ep(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout.trim();if(!t)return[];let a=null;if(t.startsWith("{"))try{a=JSON.parse(t)}catch{a=null}if(!a&&t.startsWith("{"))try{let e=await h("plutil",["-convert","json","-o","-","-"],{allowFailure:!0,stdin:t});0===e.exitCode&&e.stdout.trim().startsWith("{")&&(a=JSON.parse(e.stdout))}catch{a=null}return a?Object.entries(a).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e})):[]}async function ef(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function eh(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices??{})){let a=t.find(t=>t.udid===e);if(a)return a.state}}catch{}return null}let em=new Map;async function ew(e,t,a={}){var i;return"snapshot"===(i=t.command)||"findText"===i||"listTappables"===i||"alert"===i?N(()=>eg(e,t,a),{shouldRetry:eS}):eg(e,t,a)}async function eg(e,t,a={}){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await eI(e,a),o=await eD(e,i.port,t,a.logPath),n=await o.text(),r={};try{r=JSON.parse(n)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:n})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return r.data??{}}catch(o){let i=o instanceof f?o:new f("COMMAND_FAILED",String(o));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await ev(e.id);let i=await eI(e,a),o=await eD(e,i.port,t,a.logPath),n=await o.text(),r={};try{r=JSON.parse(n)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:n})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return r.data??{}}throw o}}async function ev(e){let t=em.get(e);if(t){try{await eD(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}ex(t.xctestrunPath),ex(t.jsonPath),em.delete(e)}}async function ey(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eI(e,t){let a=em.get(e.id);if(a)return a;await ey(e.id);let i=await eN(e.id,t),o=await eP(),n=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"300",{xctestrunPath:s,jsonPath:l}=await eO(i,{AGENT_DEVICE_RUNNER_PORT:String(o),AGENT_DEVICE_RUNNER_TIMEOUT:n},`session-${e.id}-${o}`),c=r("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(o),AGENT_DEVICE_RUNNER_TIMEOUT:n}}),u={device:e,deviceId:e.id,port:o,xctestrunPath:s,jsonPath:l,testPromise:c};return em.set(e.id,u),u}async function eN(e,t){let a,i=n.join(p.homedir(),".agent-device","ios-runner"),o=n.join(i,"derived");if((a=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(a.toLowerCase()))try{d.rmSync(o,{recursive:!0,force:!0})}catch{}let s=eA(o);if(s)return s;let l=function(){let e=n.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=n.join(t,"package.json");if(d.existsSync(e))return t;t=n.dirname(t)}return e}(),u=n.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!d.existsSync(u))throw new f("COMMAND_FAILED","iOS runner project not found",{projectPath:u});try{await r("xcodebuild",["build-for-testing","-project",u,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",o],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)}})}catch(a){let e=a instanceof f?a:new f("COMMAND_FAILED",String(a));throw new f("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=eA(o);if(!h)throw new f("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function eA(e){if(!d.existsSync(e))return null;let t=[],a=[e];for(;a.length>0;){let e=a.pop();for(let i of d.readdirSync(e,{withFileTypes:!0})){let o=n.join(e,i.name);if(i.isDirectory()){a.push(o);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=d.statSync(o);t.push({path:o,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function eb(e,t,a,i){t&&d.appendFileSync(t,e),a&&d.appendFileSync(a,e),i&&process.stderr.write(e)}function eS(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.message??""}`.toLowerCase();return!!(t.includes("runner did not accept connection")||t.includes("fetch failed")||t.includes("econnrefused")||t.includes("socket hang up"))}async function eD(e,t,a,i){let o=Date.now(),n=null;for(;Date.now()-o<15e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})}catch(e){n=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await ek(e.id,t,a);return new Response(i.body,{status:i.status})}throw new f("COMMAND_FAILED","Runner did not accept connection",{port:t,logPath:i,lastError:n?String(n):void 0})}async function ek(e,t,a){let i=JSON.stringify(a),o=await h("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",i,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),n=o.stdout;if(0!==o.exitCode)throw new f("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:o.stdout,stderr:o.stderr,exitCode:o.exitCode});return{status:200,body:n}}async function eP(){return await new Promise((e,t)=>{let a=m.createServer();a.listen(0,"127.0.0.1",()=>{let i=a.address();a.close(),"object"==typeof i&&i?.port?e(i.port):t(new f("COMMAND_FAILED","Failed to allocate port"))}),a.on("error",t)})}async function eO(e,t,a){let i,o=n.dirname(e),r=a.replace(/[^a-zA-Z0-9._-]/g,"_"),s=n.join(o,`AgentDeviceRunner.env.${r}.json`),l=n.join(o,`AgentDeviceRunner.env.${r}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new f("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new f("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let u=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},p=i.TestConfigurations;if(Array.isArray(p))for(let e of p){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&u(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(u(t),i[e]=t);d.writeFileSync(s,JSON.stringify(i,null,2));let m=await h("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==m.exitCode)throw new f("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function ex(e){try{d.existsSync(e)&&d.unlinkSync(e)}catch{}}async function e_(e,t={}){let a,i;if("ios"!==e.platform||"simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let o=await eL(),n=await N(async()=>{var e,a;let i,n,r,s=await h(o,[],{allowFailure:!0});if(t.traceLogPath&&(e=t.traceLogPath,i=((a=s).stdout??"").toString(),n=(a.stderr??"").toString(),r=`
|
|
1
|
+
let e,t;import a from"node:crypto";import{isCancel as i,select as o}from"@clack/prompts";import{node_path as n,runCmdStreaming as r,promises as s,asAppError as l,fileURLToPath as c,runCmdBackground as u,node_fs as d,node_os as p,errors_AppError as f,runCmd as h,node_net as m,whichCmd as w}from"./861.js";async function g(e,t){let a=e,n=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(a=a.filter(e=>e.platform===t.platform)),t.udid){let e=a.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=a.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=n(t.deviceName),i=a.find(t=>n(t.name)===e);if(!i)throw new f("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===a.length)return a[0];if(0===a.length)throw new f("DEVICE_NOT_FOUND","No devices found",{selector:t});let r=a.filter(e=>e.booted);if(1===r.length)return r[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await o({message:"Multiple devices available. Choose a device to continue:",options:(r.length>0?r:a).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new f("INVALID_ARGS","Device selection cancelled");if(e){let t=a.find(t=>t.id===e);if(t)return t}}return r[0]??a[0]}async function v(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let a of e){if(!a||a.startsWith("List of devices"))continue;let e=a.split(/\s+/),i=e[0];if("device"!==e[1])continue;let o=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||i;if(i.startsWith("emulator-")){let e=await h("adb",["-s",i,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(o=t.replace(/_/g," "))}let n=await y(i);t.push({platform:"android",id:i,name:o,kind:i.startsWith("emulator-")?"emulator":"device",booted:n})}return t}async function y(e){try{let t=await h("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}async function I(e,t=6e4){let a=Date.now();for(;Date.now()-a<t;){if(await y(e))return;await new Promise(e=>setTimeout(e,1e3))}throw new f("COMMAND_FAILED","Android device did not finish booting in time",{serial:e,timeoutMs:t})}async function N(e,t={}){let a,i=t.attempts??3,o=t.baseDelayMs??200,n=t.maxDelayMs??2e3,r=t.jitter??.2;for(let s=1;s<=i;s+=1)try{return await e()}catch(l){if(a=l,s>=i||t.shouldRetry&&!t.shouldRetry(l,s))break;let e=function(e,t,a,i){let o=Math.min(t,e*2**(i-1));return Math.max(0,o+o*a*(2*Math.random()-1))}(o,n,r,s);await function(e){return new Promise(t=>setTimeout(t,e))}(e)}if(a)throw a;throw new f("COMMAND_FAILED","retry failed")}let A={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function S(e,t){let a=t.trim();if(a.includes("."))return{type:"package",value:a};let i=A[a.toLowerCase()];if(i)return i;let o=(await h("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(a.toLowerCase()));if(1===o.length)return{type:"package",value:o[0]};if(o.length>1)throw new f("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:o});throw new f("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function D(e,t="launchable"){if("launchable"===t){let t=await h("adb",b(e,["shell","cmd","package","query-activities","--brief","-a","android.intent.action.MAIN","-c","android.intent.category.LAUNCHER"]),{allowFailure:!0});if(0===t.exitCode&&t.stdout.trim().length>0){let e=new Set;for(let a of t.stdout.split("\n")){let t=a.trim();if(!t)continue;let i=t.split(/\s+/)[0],o=i.includes("/")?i.split("/")[0]:i;o&&e.add(o)}if(e.size>0)return Array.from(e)}}return(await h("adb",b(e,"user-installed"===t?["shell","pm","list","packages","-3"]:["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean)}async function k(e,t="launchable"){let a=await D(e,t),i=new Set("launchable"===t?a:await D(e,"launchable"));return a.map(e=>({package:e,launchable:i.has(e)}))}async function P(e){let t=await O(e,[["shell","dumpsys","window","windows"],["shell","dumpsys","window"]]);if(t)return t;let a=await O(e,[["shell","dumpsys","activity","activities"],["shell","dumpsys","activity"]]);return a||{}}async function O(e,t){for(let a of t){let t=function(e){for(let t of[/mCurrentFocus=Window\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mFocusedApp=AppWindowToken\{[^}]*\s([\w.]+)\/([\w.$]+)/,/mResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/,/ResumedActivity:.*?\s([\w.]+)\/([\w.$]+)/]){let a=t.exec(e);if(a)return{package:a[1],activity:a[2]}}return null}((await h("adb",b(e,a),{allowFailure:!0})).stdout??"");if(t)return t}return null}async function x(e,t){e.booted||await I(e.id);let a=await S(e,t);"intent"===a.type?await h("adb",b(e,["shell","am","start","-a",a.value])):await h("adb",b(e,["shell","monkey","-p",a.value,"-c","android.intent.category.LAUNCHER","1"]))}async function _(e){e.booted||await I(e.id)}async function L(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",b(e,["shell","am","force-stop","com.android.settings"]));let a=await S(e,t);if("intent"===a.type)throw new f("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",b(e,["shell","am","force-stop",a.value]))}async function E(e,t,a){await h("adb",b(e,["shell","input","tap",String(t),String(a)]))}async function R(e){await h("adb",b(e,["shell","input","keyevent","4"]))}async function C(e){await h("adb",b(e,["shell","input","keyevent","3"]))}async function M(e){await h("adb",b(e,["shell","input","keyevent","187"]))}async function T(e,t,a,i=800){await h("adb",b(e,["shell","input","swipe",String(t),String(a),String(t),String(a),String(i)]))}async function F(e,t){let a=t.replace(/ /g,"%s");await h("adb",b(e,["shell","input","text",a]))}async function B(e,t,a){await E(e,t,a)}async function $(e,t,a,i){await B(e,t,a),await F(e,i)}async function U(e,t,a=.6){let{width:i,height:o}=await W(e),n=Math.floor(i*a),r=Math.floor(o*a),s=Math.floor(i/2),l=Math.floor(o/2),c=s,u=l,d=s,p=l;switch(t){case"up":u=l-Math.floor(r/2),p=l+Math.floor(r/2);break;case"down":u=l+Math.floor(r/2),p=l-Math.floor(r/2);break;case"left":c=s-Math.floor(n/2),d=s+Math.floor(n/2);break;case"right":c=s+Math.floor(n/2),d=s-Math.floor(n/2);break;default:throw new f("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",b(e,["shell","input","swipe",String(c),String(u),String(d),String(p),"300"]))}async function V(e,t){for(let a=0;a<8;a+=1){let a="";try{a=await X(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new f("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let a=t.toLowerCase(),i=/<node[^>]+>/g,o=i.exec(e);for(;o;){let t=o[0],n=/text="([^"]*)"/.exec(t),r=/content-desc="([^"]*)"/.exec(t),s=(n?.[1]??"").toLowerCase(),l=(r?.[1]??"").toLowerCase();if(s.includes(a)||l.includes(a)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),a=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((a+Number(e[4]))/2)}}return{x:0,y:0}}o=i.exec(e)}return null}(a,t))return;await U(e,"down",.5)}throw new f("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function j(e,t){let a=await h("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!a.stdoutBuffer)throw new f("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,a.stdoutBuffer)}async function G(e,t,a){let i=t.toLowerCase(),o=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(i){case"wifi":return void await h("adb",b(e,["shell","svc","wifi",o?"enable":"disable"]));case"airplane":await h("adb",b(e,["shell","settings","put","global","airplane_mode_on",o?"1":"0"])),await h("adb",b(e,["shell","am","broadcast","-a","android.intent.action.AIRPLANE_MODE","--ez","state",o?"true":"false"]));return;case"location":return void await h("adb",b(e,["shell","settings","put","secure","location_mode",o?"3":"0"]));default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}async function q(e,t={}){return function(e,t,a){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},a=[t],i=/<node\b[^>]*>|<\/node>/g,o=i.exec(e);for(;o;){let t=o[0];if(t.startsWith("</node")){a.length>1&&a.pop(),o=i.exec(e);continue}let n=function(e){let t=t=>{let a=RegExp(`${t}="([^"]*)"`).exec(e);return a?a[1]:null},a=e=>{let a=t(e);if(null!==a)return"true"===a};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:a("clickable"),enabled:a("enabled"),focusable:a("focusable")}}(t),r=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let a=Number(t[1]),i=Number(t[2]);return{x:a,y:i,width:Math.max(0,Number(t[3])-a),height:Math.max(0,Number(t[4])-i)}}(n.bounds),s=a[a.length-1],l={type:n.className,label:n.text||n.desc,value:n.text,identifier:n.resourceId,rect:r,enabled:n.enabled,hittable:n.clickable??n.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||a.push(l),o=i.exec(e)}return t}(e),o=[],n=!1,r=a.depth??1/0,s=a.scope?function(e,t){let a=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",o=e.value?.toLowerCase()??"",n=e.identifier?.toLowerCase()??"";if(t.includes(a)||o.includes(a)||n.includes(a))return e;i.push(...e.children)}return null}(i,a.scope):null,l=s?[s]:i.children,c=(e,t)=>{if(o.length>=800){n=!0;return}if(!(t>r)){for(let i of((a.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),a=!!(e.identifier&&e.identifier.trim().length>0);return t||a||!!e.hittable}return!0}(e,a))&&o.push({index:o.length,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:e.parentIndex}),e.children))if(c(i,t+1),n)return}};for(let e of l)if(c(e,0),n)break;return n?{nodes:o,truncated:n}:{nodes:o}}(await X(e),800,t)}async function J(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH")}async function W(e){let t=(await h("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new f("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function X(e){return N(()=>z(e),{shouldRetry:H})}async function z(e){return await h("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}function H(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.details?.stderr??""}`.toLowerCase();return!!(t.includes("device offline")||t.includes("device not found")||t.includes("transport error")||t.includes("connection reset")||t.includes("broken pipe")||t.includes("timed out"))}async function Z(){if("darwin"!==process.platform)throw new f("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await w("xcrun"))throw new f("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices))for(let a of t)a.isAvailable&&e.push({platform:"ios",id:a.udid,name:a.name,kind:"simulator",booted:"Booted"===a.state})}catch(e){throw new f("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await w("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let a of JSON.parse(t.stdout).devices??[])a.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:a.identifier,name:a.name,kind:"device",booted:!0})}catch{}return e}let Y={settings:"com.apple.Preferences"};async function K(e,t){let a=t.trim();if(a.includes("."))return a;let i=Y[a.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await ep(e)).filter(e=>e.name.toLowerCase()===a.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new f("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new f("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function Q(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}),await h("xcrun",["simctl","launch",e.id,a]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,a])}async function ee(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await ef(e),await h("open",["-a","Simulator"],{allowFailure:!0}))}async function et(e,t){let a=await K(e,t);if("simulator"===e.kind){await ef(e);let t=await h("xcrun",["simctl","terminate",e.id,a],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new f("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,a],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,a])}async function ea(e,t,a){throw ed(e,"press"),new f("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function ei(e,t,a,i=800){throw ed(e,"long-press"),new f("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function eo(e,t,a){await ea(e,t,a)}async function en(e,t){throw ed(e,"type"),new f("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function er(e,t,a,i){await eo(e,t,a),await en(e,i)}async function es(e,t,a=.6){throw ed(e,"scroll"),new f("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function el(e){throw new f("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function ec(e,t){if("simulator"===e.kind){await ef(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}async function eu(e,t,a,i){ed(e,"settings"),await ef(e);let o=t.toLowerCase(),n=function(e){let t=e.toLowerCase();if("on"===t||"true"===t||"1"===t)return!0;if("off"===t||"false"===t||"0"===t)return!1;throw new f("INVALID_ARGS",`Invalid setting state: ${e}`)}(a);switch(o){case"wifi":return void await h("xcrun",["simctl","status_bar",e.id,"override","--wifiMode",n?"active":"failed"]);case"airplane":n?await h("xcrun",["simctl","status_bar",e.id,"override","--dataNetwork","hide","--wifiMode","failed","--wifiBars","0","--cellularMode","failed","--cellularBars","0","--operatorName",""]):await h("xcrun",["simctl","status_bar",e.id,"clear"]);return;case"location":if(!i)throw new f("INVALID_ARGS","location setting requires an active app in session");await h("xcrun",["simctl","privacy",e.id,n?"grant":"revoke","location",i]);return;default:throw new f("INVALID_ARGS",`Unsupported setting: ${t}`)}}function ed(e,t){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function ep(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout.trim();if(!t)return[];let a=null;if(t.startsWith("{"))try{a=JSON.parse(t)}catch{a=null}if(!a&&t.startsWith("{"))try{let e=await h("plutil",["-convert","json","-o","-","-"],{allowFailure:!0,stdin:t});0===e.exitCode&&e.stdout.trim().startsWith("{")&&(a=JSON.parse(e.stdout))}catch{a=null}return a?Object.entries(a).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e})):[]}async function ef(e){"simulator"!==e.kind||"Booted"!==await eh(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function eh(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let a=JSON.parse(t.stdout);for(let t of Object.values(a.devices??{})){let a=t.find(t=>t.udid===e);if(a)return a.state}}catch{}return null}let em=new Map;async function ew(e,t,a={}){var i;return"snapshot"===(i=t.command)||"findText"===i||"listTappables"===i||"alert"===i?N(()=>eg(e,t,a),{shouldRetry:eS}):eg(e,t,a)}async function eg(e,t,a={}){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await eI(e,a),o=await eD(e,i.port,t,a.logPath),n=await o.text(),r={};try{r=JSON.parse(n)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:n})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return r.data??{}}catch(o){let i=o instanceof f?o:new f("COMMAND_FAILED",String(o));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await ev(e.id);let i=await eI(e,a),o=await eD(e,i.port,t,a.logPath),n=await o.text(),r={};try{r=JSON.parse(n)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:n})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:a.logPath});return r.data??{}}throw o}}async function ev(e){let t=em.get(e);if(t){try{await eD(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}ex(t.xctestrunPath),ex(t.jsonPath),em.delete(e)}}async function ey(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eI(e,t){let a=em.get(e.id);if(a)return a;await ey(e.id);let i=await eN(e.id,t),o=await eP(),n=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"0",{xctestrunPath:s,jsonPath:l}=await eO(i,{AGENT_DEVICE_RUNNER_PORT:String(o),AGENT_DEVICE_RUNNER_TIMEOUT:n},`session-${e.id}-${o}`),c=r("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(o),AGENT_DEVICE_RUNNER_TIMEOUT:n}}),u={device:e,deviceId:e.id,port:o,xctestrunPath:s,jsonPath:l,testPromise:c};return em.set(e.id,u),u}async function eN(e,t){let a,i=n.join(p.homedir(),".agent-device","ios-runner"),o=n.join(i,"derived");if((a=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(a.toLowerCase()))try{d.rmSync(o,{recursive:!0,force:!0})}catch{}let s=eA(o);if(s)return s;let l=function(){let e=n.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=n.join(t,"package.json");if(d.existsSync(e))return t;t=n.dirname(t)}return e}(),u=n.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!d.existsSync(u))throw new f("COMMAND_FAILED","iOS runner project not found",{projectPath:u});try{await r("xcodebuild",["build-for-testing","-project",u,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",o],{onStdoutChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)},onStderrChunk:e=>{eb(e,t.logPath,t.traceLogPath,t.verbose)}})}catch(a){let e=a instanceof f?a:new f("COMMAND_FAILED",String(a));throw new f("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=eA(o);if(!h)throw new f("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function eA(e){if(!d.existsSync(e))return null;let t=[],a=[e];for(;a.length>0;){let e=a.pop();for(let i of d.readdirSync(e,{withFileTypes:!0})){let o=n.join(e,i.name);if(i.isDirectory()){a.push(o);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=d.statSync(o);t.push({path:o,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function eb(e,t,a,i){t&&d.appendFileSync(t,e),a&&d.appendFileSync(a,e),i&&process.stderr.write(e)}function eS(e){if(!(e instanceof f)||"COMMAND_FAILED"!==e.code)return!1;let t=`${e.message??""}`.toLowerCase();return!!(t.includes("runner did not accept connection")||t.includes("fetch failed")||t.includes("econnrefused")||t.includes("socket hang up"))}async function eD(e,t,a,i){let o=Date.now(),n=null;for(;Date.now()-o<15e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})}catch(e){n=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await ek(e.id,t,a);return new Response(i.body,{status:i.status})}throw new f("COMMAND_FAILED","Runner did not accept connection",{port:t,logPath:i,lastError:n?String(n):void 0})}async function ek(e,t,a){let i=JSON.stringify(a),o=await h("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",i,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),n=o.stdout;if(0!==o.exitCode)throw new f("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:o.stdout,stderr:o.stderr,exitCode:o.exitCode});return{status:200,body:n}}async function eP(){return await new Promise((e,t)=>{let a=m.createServer();a.listen(0,"127.0.0.1",()=>{let i=a.address();a.close(),"object"==typeof i&&i?.port?e(i.port):t(new f("COMMAND_FAILED","Failed to allocate port"))}),a.on("error",t)})}async function eO(e,t,a){let i,o=n.dirname(e),r=a.replace(/[^a-zA-Z0-9._-]/g,"_"),s=n.join(o,`AgentDeviceRunner.env.${r}.json`),l=n.join(o,`AgentDeviceRunner.env.${r}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new f("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new f("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let u=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},p=i.TestConfigurations;if(Array.isArray(p))for(let e of p){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&u(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(u(t),i[e]=t);d.writeFileSync(s,JSON.stringify(i,null,2));let m=await h("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==m.exitCode)throw new f("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function ex(e){try{d.existsSync(e)&&d.unlinkSync(e)}catch{}}async function e_(e,t={}){let a,i;if("ios"!==e.platform||"simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let o=await eL(),n=await N(async()=>{var e,a;let i,n,r,s=await h(o,[],{allowFailure:!0});if(t.traceLogPath&&(e=t.traceLogPath,i=((a=s).stdout??"").toString(),n=(a.stderr??"").toString(),r=`
|
|
2
2
|
[axsnapshot] exit=${a.exitCode} stdoutBytes=${i.length} stderrBytes=${n.length}
|
|
3
3
|
`,d.appendFileSync(e,r),(0!==a.exitCode||n.length>0)&&(n.length>0&&d.appendFileSync(e,`${n}
|
|
4
4
|
`),0!==a.exitCode&&i.length>0&&d.appendFileSync(e,`${i}
|
package/package.json
CHANGED
|
@@ -10,7 +10,6 @@ description: Automates mobile and simulator interactions for iOS and Android dev
|
|
|
10
10
|
```bash
|
|
11
11
|
agent-device open Settings --platform ios
|
|
12
12
|
agent-device snapshot -i
|
|
13
|
-
agent-device snapshot -s @e3
|
|
14
13
|
agent-device click @e3
|
|
15
14
|
agent-device wait text "Camera"
|
|
16
15
|
agent-device alert wait 10000
|
|
@@ -21,8 +20,8 @@ agent-device close
|
|
|
21
20
|
## Core workflow
|
|
22
21
|
|
|
23
22
|
1. Open app or just boot device: `open [app]`
|
|
24
|
-
2. Snapshot: `snapshot
|
|
25
|
-
3. Interact using refs (`click @
|
|
23
|
+
2. Snapshot: `snapshot` to get full XCTest accessibility tree snapshot
|
|
24
|
+
3. Interact using refs (`click @ref`, `fill @ref "text"`)
|
|
26
25
|
4. Re-snapshot after navigation or UI changes
|
|
27
26
|
5. Close session when done
|
|
28
27
|
|
|
@@ -45,7 +44,7 @@ agent-device snapshot -c # Compact output
|
|
|
45
44
|
agent-device snapshot -d 3 # Limit depth
|
|
46
45
|
agent-device snapshot -s "Camera" # Scope to label/identifier
|
|
47
46
|
agent-device snapshot --raw # Raw node output
|
|
48
|
-
agent-device snapshot --backend xctest # XCTest snapshot (fast, complete, no permissions)
|
|
47
|
+
agent-device snapshot --backend xctest # default: XCTest snapshot (fast, complete, no permissions)
|
|
49
48
|
agent-device snapshot --backend ax # macOS Accessibility tree (fast, needs permissions, less fidelity, optional)
|
|
50
49
|
```
|
|
51
50
|
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## iOS AX snapshot
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
AX snapshot is an alternative to XCTest for when it fails (which shouldn't happen usually); it uses macOS Accessibility APIs and requires permission:
|
|
6
6
|
|
|
7
7
|
System Settings > Privacy & Security > Accessibility
|
|
8
8
|
|
|
9
|
-
If permission is missing, use:
|
|
9
|
+
If permission is missing, use XCTest backend:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
agent-device snapshot --backend xctest --platform ios
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Hybrid/AX is fast; XCTest is
|
|
15
|
+
Hybrid/AX is fast; XCTest is equally fast but does not require permissions.
|
|
16
16
|
|
|
17
17
|
## Simulator troubleshooting
|
|
18
18
|
|
|
@@ -7,7 +7,7 @@ Refs let agents interact without repeating full UI trees. Snapshot -> refs -> cl
|
|
|
7
7
|
## Snapshot
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
agent-device snapshot -i
|
|
10
|
+
agent-device snapshot -i
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Output:
|
|
@@ -24,8 +24,8 @@ App: com.apple.Preferences
|
|
|
24
24
|
## Using refs
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
agent-device click @e2
|
|
28
|
-
agent-device fill @e5 "test"
|
|
27
|
+
agent-device click @e2
|
|
28
|
+
agent-device fill @e5 "test"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Ref lifecycle
|
|
@@ -38,8 +38,8 @@ Always re-snapshot after any transition.
|
|
|
38
38
|
Use `-s` to scope to labels/identifiers. This reduces size and speeds up results:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
agent-device snapshot -i -s "Camera"
|
|
42
|
-
agent-device snapshot -i -s @e3
|
|
41
|
+
agent-device snapshot -i -s "Camera"
|
|
42
|
+
agent-device snapshot -i -s @e3
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
## Troubleshooting
|
|
@@ -173,7 +173,7 @@ async function ensureRunnerSession(
|
|
|
173
173
|
await ensureBooted(device.id);
|
|
174
174
|
const xctestrun = await ensureXctestrun(device.id, options);
|
|
175
175
|
const port = await getFreePort();
|
|
176
|
-
const runnerTimeout = process.env.AGENT_DEVICE_RUNNER_TIMEOUT ?? '
|
|
176
|
+
const runnerTimeout = process.env.AGENT_DEVICE_RUNNER_TIMEOUT ?? '0';
|
|
177
177
|
const { xctestrunPath, jsonPath } = await prepareXctestrunWithEnv(
|
|
178
178
|
xctestrun,
|
|
179
179
|
{ AGENT_DEVICE_RUNNER_PORT: String(port), AGENT_DEVICE_RUNNER_TIMEOUT: runnerTimeout },
|