agent-device
Advanced tools
Sorry, the diff of this file is not supported yet
+1
-1
@@ -1,1 +0,1 @@ | ||
| import{spawn as e}from"node:child_process";function o(e,o,t){return o in e?Object.defineProperty(e,o,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[o]=t,e}class t extends Error{constructor(e,t,r,n){super(t),o(this,"code",void 0),o(this,"details",void 0),o(this,"cause",void 0),this.code=e,this.details=r,this.cause=n}}function r(e){return e instanceof t?e:e instanceof Error?new t("UNKNOWN",e.message,void 0,e):new t("UNKNOWN","Unknown error",{err:e})}async function n(o,r,d={}){return new Promise((n,i)=>{let a=e(o,r,{cwd:d.cwd,env:d.env,stdio:["ignore","pipe","pipe"]}),u="",s=d.binaryStdout?Buffer.alloc(0):void 0,f="";d.binaryStdout||a.stdout.setEncoding("utf8"),a.stderr.setEncoding("utf8"),a.stdout.on("data",e=>{d.binaryStdout?s=Buffer.concat([s??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):u+=e}),a.stderr.on("data",e=>{f+=e}),a.on("error",e=>{"ENOENT"===e.code?i(new t("TOOL_MISSING",`${o} not found in PATH`,{cmd:o},e)):i(new t("COMMAND_FAILED",`Failed to run ${o}`,{cmd:o,args:r},e))}),a.on("close",e=>{let a=e??1;0===a||d.allowFailure?n({stdout:u,stderr:f,exitCode:a,stdoutBuffer:s}):i(new t("COMMAND_FAILED",`${o} exited with code ${a}`,{cmd:o,args:r,stdout:u,stderr:f,exitCode:a}))})})}async function d(e){try{var o;let{shell:t,args:r}=(o=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",o]}:{shell:"bash",args:["-lc",`command -v ${o}`]}),d=await n(t,r,{allowFailure:!0});return 0===d.exitCode&&d.stdout.trim().length>0}catch{return!1}}function i(o,t,r={}){e(o,t,{cwd:r.cwd,env:r.env,stdio:"ignore",detached:!0}).unref()}async function a(o,r,n={}){return new Promise((d,i)=>{let a=e(o,r,{cwd:n.cwd,env:n.env,stdio:["ignore","pipe","pipe"]}),u="",s="",f=n.binaryStdout?Buffer.alloc(0):void 0;n.binaryStdout||a.stdout.setEncoding("utf8"),a.stderr.setEncoding("utf8"),a.stdout.on("data",e=>{if(n.binaryStdout){f=Buffer.concat([f??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let o=String(e);u+=o,n.onStdoutChunk?.(o)}),a.stderr.on("data",e=>{let o=String(e);s+=o,n.onStderrChunk?.(o)}),a.on("error",e=>{"ENOENT"===e.code?i(new t("TOOL_MISSING",`${o} not found in PATH`,{cmd:o},e)):i(new t("COMMAND_FAILED",`Failed to run ${o}`,{cmd:o,args:r},e))}),a.on("close",e=>{let a=e??1;0===a||n.allowFailure?d({stdout:u,stderr:s,exitCode:a,stdoutBuffer:f}):i(new t("COMMAND_FAILED",`${o} exited with code ${a}`,{cmd:o,args:r,stdout:u,stderr:s,exitCode:a}))})})}export{fileURLToPath,pathToFileURL}from"node:url";export{default as node_net}from"node:net";export{default as node_fs,promises}from"node:fs";export{default as node_os}from"node:os";export{default as node_path}from"node:path";export{r as asAppError,t as errors_AppError,n as runCmd,i as runCmdDetached,a as runCmdStreaming,d as whichCmd}; | ||
| import{spawn as e}from"node:child_process";function t(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}class n extends Error{constructor(e,n,o,r){super(n),t(this,"code",void 0),t(this,"details",void 0),t(this,"cause",void 0),this.code=e,this.details=o,this.cause=r}}function o(e){return e instanceof n?e:e instanceof Error?new n("UNKNOWN",e.message,void 0,e):new n("UNKNOWN","Unknown error",{err:e})}async function r(t,o,d={}){return new Promise((r,i)=>{let s=e(t,o,{cwd:d.cwd,env:d.env,stdio:["pipe","pipe","pipe"]}),u="",a=d.binaryStdout?Buffer.alloc(0):void 0,c="";d.binaryStdout||s.stdout.setEncoding("utf8"),s.stderr.setEncoding("utf8"),void 0!==d.stdin&&s.stdin.write(d.stdin),s.stdin.end(),s.stdout.on("data",e=>{d.binaryStdout?a=Buffer.concat([a??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):u+=e}),s.stderr.on("data",e=>{c+=e}),s.on("error",e=>{"ENOENT"===e.code?i(new n("TOOL_MISSING",`${t} not found in PATH`,{cmd:t},e)):i(new n("COMMAND_FAILED",`Failed to run ${t}`,{cmd:t,args:o},e))}),s.on("close",e=>{let s=e??1;0===s||d.allowFailure?r({stdout:u,stderr:c,exitCode:s,stdoutBuffer:a}):i(new n("COMMAND_FAILED",`${t} exited with code ${s}`,{cmd:t,args:o,stdout:u,stderr:c,exitCode:s}))})})}async function d(e){try{var t;let{shell:n,args:o}=(t=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",t]}:{shell:"bash",args:["-lc",`command -v ${t}`]}),d=await r(n,o,{allowFailure:!0});return 0===d.exitCode&&d.stdout.trim().length>0}catch{return!1}}function i(t,n,o={}){e(t,n,{cwd:o.cwd,env:o.env,stdio:"ignore",detached:!0}).unref()}async function s(t,o,r={}){return new Promise((d,i)=>{let s=e(t,o,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"]}),u="",a="",c=r.binaryStdout?Buffer.alloc(0):void 0;r.binaryStdout||s.stdout.setEncoding("utf8"),s.stderr.setEncoding("utf8"),void 0!==r.stdin&&s.stdin.write(r.stdin),s.stdin.end(),s.stdout.on("data",e=>{if(r.binaryStdout){c=Buffer.concat([c??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let t=String(e);u+=t,r.onStdoutChunk?.(t)}),s.stderr.on("data",e=>{let t=String(e);a+=t,r.onStderrChunk?.(t)}),s.on("error",e=>{"ENOENT"===e.code?i(new n("TOOL_MISSING",`${t} not found in PATH`,{cmd:t},e)):i(new n("COMMAND_FAILED",`Failed to run ${t}`,{cmd:t,args:o},e))}),s.on("close",e=>{let s=e??1;0===s||r.allowFailure?d({stdout:u,stderr:a,exitCode:s,stdoutBuffer:c}):i(new n("COMMAND_FAILED",`${t} exited with code ${s}`,{cmd:t,args:o,stdout:u,stderr:a,exitCode:s}))})})}function u(t,o,r={}){let d=e(t,o,{cwd:r.cwd,env:r.env,stdio:["ignore","pipe","pipe"]}),i="",s="";d.stdout.setEncoding("utf8"),d.stderr.setEncoding("utf8"),d.stdout.on("data",e=>{i+=e}),d.stderr.on("data",e=>{s+=e});let a=new Promise((e,u)=>{d.on("error",e=>{"ENOENT"===e.code?u(new n("TOOL_MISSING",`${t} not found in PATH`,{cmd:t},e)):u(new n("COMMAND_FAILED",`Failed to run ${t}`,{cmd:t,args:o},e))}),d.on("close",d=>{let a=d??1;0===a||r.allowFailure?e({stdout:i,stderr:s,exitCode:a}):u(new n("COMMAND_FAILED",`${t} exited with code ${a}`,{cmd:t,args:o,stdout:i,stderr:s,exitCode:a}))})});return{child:d,wait:a}}export{fileURLToPath,pathToFileURL}from"node:url";export{default as node_net}from"node:net";export{default as node_fs,promises}from"node:fs";export{default as node_os}from"node:os";export{default as node_path}from"node:path";export{o as asAppError,n as errors_AppError,r as runCmd,u as runCmdBackground,i as runCmdDetached,s as runCmdStreaming,d as whichCmd}; |
+26
-14
@@ -1,6 +0,6 @@ | ||
| import{node_path as e,fileURLToPath as t,asAppError as r,pathToFileURL as n,runCmdDetached as s,node_fs as o,node_os as i,node_net as a,errors_AppError as c}from"./861.js";function l(e){process.stdout.write(`${JSON.stringify(e,null,2)} | ||
| `)}function u(e){let t=e.details?` | ||
| import{node_path as e,fileURLToPath as t,asAppError as r,pathToFileURL as n,runCmdDetached as s,node_fs as o,node_os as i,node_net as a,errors_AppError as l}from"./861.js";function c(e){process.stdout.write(`${JSON.stringify(e,null,2)} | ||
| `)}function d(e){let t=e.details?` | ||
| ${JSON.stringify(e.details,null,2)}`:"";process.stderr.write(`Error (${e.code}): ${e.message}${t} | ||
| `)}let d=e.join(i.homedir(),".agent-device"),p=e.join(d,"daemon.json"),f=function(){let e=process.env.AGENT_DEVICE_DAEMON_TIMEOUT_MS;if(!e)return 6e4;let t=Number(e);return Number.isFinite(t)?Math.max(1e3,Math.floor(t)):6e4}();async function m(e){let t=await h(),r={...e,token:t.token};return await b(t,r)}async function h(){let t=w(),r=function(){try{let t=x();return JSON.parse(o.readFileSync(e.join(t,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}();if(t&&t.version===r&&await y(t))return t;t&&(t.version!==r||!await y(t))&&o.existsSync(p)&&o.unlinkSync(p),await g();let n=Date.now();for(;Date.now()-n<5e3;){let e=w();if(e&&await y(e))return e;await new Promise(e=>setTimeout(e,100))}throw new c("COMMAND_FAILED","Failed to start daemon",{infoPath:p,hint:"Run pnpm build, or delete ~/.agent-device/daemon.json if stale."})}function w(){if(!o.existsSync(p))return null;try{let e=JSON.parse(o.readFileSync(p,"utf8"));if(!e.port||!e.token)return null;return e}catch{return null}}async function y(e){return new Promise(t=>{let r=a.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function g(){let t=x(),r=e.join(t,"dist","src","daemon.js"),n=e.join(t,"src","daemon.ts"),i=o.existsSync(r);if(!i&&!o.existsSync(n))throw new c("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:n});let a=i?[r]:["--experimental-strip-types",n];s(process.execPath,a)}async function b(e,t){return new Promise((r,n)=>{let s=a.createConnection({host:"127.0.0.1",port:e.port},()=>{s.write(`${JSON.stringify(t)} | ||
| `)}),o=setTimeout(()=>{s.destroy(),n(new c("COMMAND_FAILED","Daemon request timed out",{timeoutMs:f}))},f),i="";s.setEncoding("utf8"),s.on("data",e=>{let t=(i+=e).indexOf("\n");if(-1===t)return;let a=i.slice(0,t).trim();if(a)try{let e=JSON.parse(a);s.end(),clearTimeout(o),r(e)}catch(e){clearTimeout(o),n(e)}}),s.on("error",e=>{clearTimeout(o),n(e)})})}function x(){let r=e.dirname(t(import.meta.url)),n=r;for(let t=0;t<6;t+=1){let t=e.join(n,"package.json");if(o.existsSync(t))return n;n=e.dirname(n)}return r}async function S(t){let n=function(e){let t={json:!1,help:!1},r=[];for(let n=0;n<e.length;n+=1){let s=e[n];if("--json"===s){t.json=!0;continue}if("--help"===s||"-h"===s){t.help=!0;continue}if("--verbose"===s||"-v"===s){t.verbose=!0;continue}if("-i"===s){t.snapshotInteractiveOnly=!0;continue}if("-c"===s){t.snapshotCompact=!0;continue}if("--raw"===s){t.snapshotRaw=!0;continue}if("--no-record"===s){t.noRecord=!0;continue}if("--record-json"===s){t.recordJson=!0;continue}if(s.startsWith("--backend")){let r=s.includes("=")?s.split("=")[1]:e[n+1];if(s.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new c("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(s.startsWith("--")){let[r,o]=s.split("="),i=o??e[n+1];switch(!o&&(n+=1),r){case"--platform":if("ios"!==i&&"android"!==i)throw new c("INVALID_ARGS",`Invalid platform: ${i}`);t.platform=i;break;case"--depth":{let e=Number(i);if(!Number.isFinite(e)||e<0)throw new c("INVALID_ARGS",`Invalid depth: ${i}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=i;break;case"--device":t.device=i;break;case"--udid":t.udid=i;break;case"--serial":t.serial=i;break;case"--out":t.out=i;break;case"--session":t.session=i;break;default:throw new c("INVALID_ARGS",`Unknown flag: ${r}`)}continue}if("-d"===s){let r=e[n+1];n+=1;let s=Number(r);if(!Number.isFinite(s)||s<0)throw new c("INVALID_ARGS",`Invalid depth: ${r}`);t.snapshotDepth=Math.floor(s);continue}if("-s"===s){let r=e[n+1];n+=1,t.snapshotScope=r;continue}r.push(s)}return{command:r.shift()??null,positionals:r,flags:t}}(t);(n.flags.help||!n.command)&&(process.stdout.write(`agent-device <command> [args] [--json] | ||
| `)}let u=e.join(i.homedir(),".agent-device"),p=e.join(u,"daemon.json"),f=function(){let e=process.env.AGENT_DEVICE_DAEMON_TIMEOUT_MS;if(!e)return 6e4;let t=Number(e);return Number.isFinite(t)?Math.max(1e3,Math.floor(t)):6e4}();async function m(e){let t=await h(),r={...e,token:t.token};return await b(t,r)}async function h(){let t=w(),r=function(){try{let t=x();return JSON.parse(o.readFileSync(e.join(t,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}();if(t&&t.version===r&&await y(t))return t;t&&(t.version!==r||!await y(t))&&o.existsSync(p)&&o.unlinkSync(p),await g();let n=Date.now();for(;Date.now()-n<5e3;){let e=w();if(e&&await y(e))return e;await new Promise(e=>setTimeout(e,100))}throw new l("COMMAND_FAILED","Failed to start daemon",{infoPath:p,hint:"Run pnpm build, or delete ~/.agent-device/daemon.json if stale."})}function w(){if(!o.existsSync(p))return null;try{let e=JSON.parse(o.readFileSync(p,"utf8"));if(!e.port||!e.token)return null;return e}catch{return null}}async function y(e){return new Promise(t=>{let r=a.createConnection({host:"127.0.0.1",port:e.port},()=>{r.destroy(),t(!0)});r.on("error",()=>{t(!1)})})}async function g(){let t=x(),r=e.join(t,"dist","src","daemon.js"),n=e.join(t,"src","daemon.ts"),i=o.existsSync(r);if(!i&&!o.existsSync(n))throw new l("COMMAND_FAILED","Daemon entry not found",{distPath:r,srcPath:n});let a=i?[r]:["--experimental-strip-types",n];s(process.execPath,a)}async function b(e,t){return new Promise((r,n)=>{let s=a.createConnection({host:"127.0.0.1",port:e.port},()=>{s.write(`${JSON.stringify(t)} | ||
| `)}),o=setTimeout(()=>{s.destroy(),n(new l("COMMAND_FAILED","Daemon request timed out",{timeoutMs:f}))},f),i="";s.setEncoding("utf8"),s.on("data",e=>{let t=(i+=e).indexOf("\n");if(-1===t)return;let a=i.slice(0,t).trim();if(a)try{let e=JSON.parse(a);s.end(),clearTimeout(o),r(e)}catch(e){clearTimeout(o),n(e)}}),s.on("error",e=>{clearTimeout(o),n(e)})})}function x(){let r=e.dirname(t(import.meta.url)),n=r;for(let t=0;t<6;t+=1){let t=e.join(n,"package.json");if(o.existsSync(t))return n;n=e.dirname(n)}return r}async function S(t){let n=function(e){let t={json:!1,help:!1},r=[];for(let n=0;n<e.length;n+=1){let s=e[n];if("--json"===s){t.json=!0;continue}if("--help"===s||"-h"===s){t.help=!0;continue}if("--verbose"===s||"-v"===s){t.verbose=!0;continue}if("-i"===s){t.snapshotInteractiveOnly=!0;continue}if("-c"===s){t.snapshotCompact=!0;continue}if("--raw"===s){t.snapshotRaw=!0;continue}if("--no-record"===s){t.noRecord=!0;continue}if("--record-json"===s){t.recordJson=!0;continue}if("--user-installed"===s){t.appsFilter="user-installed";continue}if("--all"===s){t.appsFilter="all";continue}if(s.startsWith("--backend")){let r=s.includes("=")?s.split("=")[1]:e[n+1];if(s.includes("=")||(n+=1),"ax"!==r&&"xctest"!==r)throw new l("INVALID_ARGS",`Invalid backend: ${r}`);t.snapshotBackend=r;continue}if(s.startsWith("--")){let[r,o]=s.split("="),i=o??e[n+1];switch(!o&&(n+=1),r){case"--platform":if("ios"!==i&&"android"!==i)throw new l("INVALID_ARGS",`Invalid platform: ${i}`);t.platform=i;break;case"--depth":{let e=Number(i);if(!Number.isFinite(e)||e<0)throw new l("INVALID_ARGS",`Invalid depth: ${i}`);t.snapshotDepth=Math.floor(e);break}case"--scope":t.snapshotScope=i;break;case"--device":t.device=i;break;case"--udid":t.udid=i;break;case"--serial":t.serial=i;break;case"--out":t.out=i;break;case"--session":t.session=i;break;default:throw new l("INVALID_ARGS",`Unknown flag: ${r}`)}continue}if("-d"===s){let r=e[n+1];n+=1;let s=Number(r);if(!Number.isFinite(s)||s<0)throw new l("INVALID_ARGS",`Invalid depth: ${r}`);t.snapshotDepth=Math.floor(s);continue}if("-s"===s){let r=e[n+1];n+=1,t.snapshotScope=r;continue}r.push(s)}return{command:r.shift()??null,positionals:r,flags:t}}(t);(n.flags.help||!n.command)&&(process.stdout.write(`agent-device <command> [args] [--json] | ||
@@ -21,4 +21,10 @@ CLI to control iOS and Android devices for AI agents. | ||
| xctest: XCTest snapshot (slower, no permissions) | ||
| devices List available devices | ||
| apps [--user-installed|--all] List installed apps (Android launchable by default, iOS simulator) | ||
| back Navigate back (where supported) | ||
| home Go to home screen (where supported) | ||
| app-switcher Open app switcher (where supported) | ||
| wait <ms>|text <text>|@ref [timeoutMs] Wait for duration or text to appear | ||
| alert [get|accept|dismiss|wait] [timeout] Inspect or handle alert (iOS simulator) | ||
| click <@ref> Click element by snapshot ref | ||
| rect <label|@ref> Fetch element frame by label or ref (iOS sim) | ||
| get text <@ref> Return element text by ref | ||
@@ -35,2 +41,4 @@ get attrs <@ref> Return element attributes by ref | ||
| screenshot [--out path] Capture screenshot | ||
| record start [path] Start screen recording | ||
| record stop Stop screen recording | ||
| session list List active sessions | ||
@@ -49,16 +57,20 @@ | ||
| --record-json Record JSON session log | ||
| --user-installed Apps: list user-installed packages (Android only) | ||
| --all Apps: list all packages (Android only) | ||
| `),process.exit(+!n.flags.help));let{command:s,positionals:a,flags:d}=n,p=d.session??process.env.AGENT_DEVICE_SESSION??"default",f=d.verbose&&!d.json?function(){try{let t=e.join(i.homedir(),".agent-device","daemon.log"),r=0,n=!1,s=setInterval(()=>{if(n||!o.existsSync(t))return;let e=o.statSync(t);if(e.size<=r)return;let s=o.openSync(t,"r"),i=Buffer.alloc(e.size-r);o.readSync(s,i,0,i.length,r),o.closeSync(s),r=e.size,i.length>0&&process.stdout.write(i.toString("utf8"))},200);return()=>{n=!0,clearInterval(s)}}catch{return null}}():null;try{if("session"===s){let e=a[0]??"list";if("list"!==e)throw new c("INVALID_ARGS","session only supports list");let t=await m({session:p,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new c(t.error.code,t.error.message);d.json?l({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)} | ||
| `),f&&f();return}let e=await m({session:p,command:s,positionals:a,flags:d});if(e.ok){if(d.json){l({success:!0,data:e.data??{}}),f&&f();return}if("snapshot"===s){process.stdout.write(function(e,t={}){let r=e.nodes??[],n=!!e.truncated,s="string"==typeof e.appName?e.appName:void 0,o="string"==typeof e.appBundleId?e.appBundleId:void 0,i=[];s&&i.push(`Page: ${s}`),o&&i.push(`App: ${o}`);let a=`Snapshot: ${r.length} nodes${n?" (truncated)":""}`,c=i.length>0?`${i.join("\n")} | ||
| `:"";if(!Array.isArray(r)||0===r.length)return`${c}${a} | ||
| `;if(t.raw){let e=r.map(e=>JSON.stringify(e));return`${c}${a} | ||
| `),process.exit(+!n.flags.help));let{command:s,positionals:a,flags:u}=n,p=u.session??process.env.AGENT_DEVICE_SESSION??"default",f=u.verbose&&!u.json?function(){try{let t=e.join(i.homedir(),".agent-device","daemon.log"),r=0,n=!1,s=setInterval(()=>{if(n||!o.existsSync(t))return;let e=o.statSync(t);if(e.size<=r)return;let s=o.openSync(t,"r"),i=Buffer.alloc(e.size-r);o.readSync(s,i,0,i.length,r),o.closeSync(s),r=e.size,i.length>0&&process.stdout.write(i.toString("utf8"))},200);return()=>{n=!0,clearInterval(s)}}catch{return null}}():null;try{if("session"===s){let e=a[0]??"list";if("list"!==e)throw new l("INVALID_ARGS","session only supports list");let t=await m({session:p,command:"session_list",positionals:[],flags:{}});if(!t.ok)throw new l(t.error.code,t.error.message);u.json?c({success:!0,data:t.data??{}}):process.stdout.write(`${JSON.stringify(t.data??{},null,2)} | ||
| `),f&&f();return}let e=await m({session:p,command:s,positionals:a,flags:u});if(e.ok){if(u.json){c({success:!0,data:e.data??{}}),f&&f();return}if("snapshot"===s){process.stdout.write(function(e,t={}){let r=e.nodes??[],n=!!e.truncated,s="string"==typeof e.appName?e.appName:void 0,o="string"==typeof e.appBundleId?e.appBundleId:void 0,i=[];s&&i.push(`Page: ${s}`),o&&i.push(`App: ${o}`);let a=`Snapshot: ${r.length} nodes${n?" (truncated)":""}`,l=i.length>0?`${i.join("\n")} | ||
| `:"";if(!Array.isArray(r)||0===r.length)return`${l}${a} | ||
| `;if(t.raw){let e=r.map(e=>JSON.stringify(e));return`${l}${a} | ||
| ${e.join("\n")} | ||
| `}let l=[],u=[];for(let e of r){let t=e.depth??0;for(;l.length>0&&t<=l[l.length-1];)l.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"statictext":return"text";case"textfield":case"textfield":return"text-field";case"textview":case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":return"image";case"table":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"group":return"group";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":return"scroll-area";case"table":return"table";default:return t||"element"}}(e.type??"Element"),s="group"===n&&!r;s&&l.push(t);let o=s?t:Math.max(0,t-l.length),i=" ".repeat(o),a=e.ref?`@${e.ref}`:"",c=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),d=c?` [${c}]`:"",p=r?` "${r}"`:"";if(s){u.push(`${i}${a} [${n}]${d}`.trimEnd());continue}u.push(`${i}${a} [${n}]${p}${d}`.trimEnd())}return`${c}${a} | ||
| ${u.join("\n")} | ||
| `}(e.data??{},{raw:d.snapshotRaw})),f&&f();return}if("get"===s){let t=a[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t} | ||
| `}let c=[],d=[];for(let e of r){let t=e.depth??0;for(;c.length>0&&t<=c[c.length-1];)c.pop();let r=e.label?.trim()||e.value?.trim()||e.identifier?.trim()||"",n=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();switch(t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"statictext":return"text";case"textfield":case"textfield":return"text-field";case"textview":case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":return"image";case"table":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"group":return"group";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":return"scroll-area";case"table":return"table";default:return t||"element"}}(e.type??"Element"),s="group"===n&&!r;s&&c.push(t);let o=s?t:Math.max(0,t-c.length),i=" ".repeat(o),a=e.ref?`@${e.ref}`:"",l=[!1===e.enabled?"disabled":null].filter(Boolean).join(", "),u=l?` [${l}]`:"",p=r?` "${r}"`:"";if(s){d.push(`${i}${a} [${n}]${u}`.trimEnd());continue}d.push(`${i}${a} [${n}]${p}${u}`.trimEnd())}return`${l}${a} | ||
| ${d.join("\n")} | ||
| `}(e.data??{},{raw:u.snapshotRaw})),f&&f();return}if("get"===s){let t=a[0];if("text"===t){let t=e.data?.text??"";process.stdout.write(`${t} | ||
| `),f&&f();return}if("attrs"===t){let t=e.data?.node??{};process.stdout.write(`${JSON.stringify(t,null,2)} | ||
| `),f&&f();return}}if("click"===s){let t=e.data?.ref??"",r=e.data?.x,n=e.data?.y;t&&"number"==typeof r&&"number"==typeof n&&process.stdout.write(`Clicked @${t} (${r}, ${n}) | ||
| `),f&&f();return}f&&f();return}throw new c(e.error.code,e.error.message,e.error.details)}catch(t){let e=r(t);if(d.json)l({success:!1,error:{code:e.code,message:e.message,details:e.details}});else if(u(e),d.verbose)try{let e=await import("node:fs"),t=await import("node:os"),r=(await import("node:path")).join(t.homedir(),".agent-device","daemon.log");if(e.existsSync(r)){let t=e.readFileSync(r,"utf8").split("\n"),n=t.slice(Math.max(0,t.length-200)).join("\n");n.trim().length>0&&process.stderr.write(` | ||
| `),f&&f();return}if(e.data&&"object"==typeof e.data){let t=e.data;if("devices"===s){let e=(Array.isArray(t.devices)?t.devices:[]).map(e=>{let t=e?.name??e?.id??"unknown",r=e?.platform??"unknown",n=e?.kind?` ${e.kind}`:"",s="boolean"==typeof e?.booted?` booted=${e.booted}`:"";return`${t} (${r}${n})${s}`});process.stdout.write(`${e.join("\n")} | ||
| `),f&&f();return}if("apps"===s){let e=Array.isArray(t.apps)?t.apps:[];process.stdout.write(`${e.join("\n")} | ||
| `),f&&f();return}}f&&f();return}throw new l(e.error.code,e.error.message,e.error.details)}catch(t){let e=r(t);if(u.json)c({success:!1,error:{code:e.code,message:e.message,details:e.details}});else if(d(e),u.verbose)try{let e=await import("node:fs"),t=await import("node:os"),r=(await import("node:path")).join(t.homedir(),".agent-device","daemon.log");if(e.existsSync(r)){let t=e.readFileSync(r,"utf8").split("\n"),n=t.slice(Math.max(0,t.length-200)).join("\n");n.trim().length>0&&process.stderr.write(` | ||
| [daemon log] | ||
| ${n} | ||
| `)}}catch{}f&&f(),process.exit(1)}}n(process.argv[1]??"").href===import.meta.url&&S(process.argv.slice(2)).catch(e=>{u(r(e)),process.exit(1)}),S(process.argv.slice(2)); | ||
| `)}}catch{}f&&f(),process.exit(1)}}n(process.argv[1]??"").href===import.meta.url&&S(process.argv.slice(2)).catch(e=>{d(r(e)),process.exit(1)}),S(process.argv.slice(2)); |
@@ -1,5 +0,5 @@ | ||
| let e,t;import n from"node:crypto";import{isCancel as i,select as r}from"@clack/prompts";import{node_path as a,runCmdStreaming as o,promises as s,asAppError as l,fileURLToPath as c,node_fs as u,node_os as d,node_net as f,errors_AppError as p,runCmd as h,whichCmd as m}from"./861.js";async function w(e,t){let n=e,a=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(n=n.filter(e=>e.platform===t.platform)),t.udid){let e=n.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=n.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new p("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=a(t.deviceName),i=n.find(t=>a(t.name)===e);if(!i)throw new p("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return i}if(1===n.length)return n[0];if(0===n.length)throw new p("DEVICE_NOT_FOUND","No devices found",{selector:t});let o=n.filter(e=>e.booted);if(1===o.length)return o[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await r({message:"Multiple devices available. Choose a device to continue:",options:(o.length>0?o:n).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(i(e))throw new p("INVALID_ARGS","Device selection cancelled");if(e){let t=n.find(t=>t.id===e);if(t)return t}}return o[0]??n[0]}async function g(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH");let e=(await h("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let n of e){if(!n||n.startsWith("List of devices"))continue;let e=n.split(/\s+/),i=e[0];if("device"!==e[1])continue;let r=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||i;if(i.startsWith("emulator-")){let e=await h("adb",["-s",i,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(r=t.replace(/_/g," "))}let a=await y(i);t.push({platform:"android",id:i,name:r,kind:i.startsWith("emulator-")?"emulator":"device",booted:a})}return t}async function y(e){try{let t=await h("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}async function N(e,t=6e4){let n=Date.now();for(;Date.now()-n<t;){if(await y(e))return;await new Promise(e=>setTimeout(e,1e3))}throw new p("COMMAND_FAILED","Android device did not finish booting in time",{serial:e,timeoutMs:t})}let v={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function S(e,t){let n=t.trim();if(n.includes("."))return{type:"package",value:n};let i=v[n.toLowerCase()];if(i)return i;let r=(await h("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(n.toLowerCase()));if(1===r.length)return{type:"package",value:r[0]};if(r.length>1)throw new p("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:r});throw new p("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function A(e,t){e.booted||await N(e.id);let n=await S(e,t);"intent"===n.type?await h("adb",b(e,["shell","am","start","-a",n.value])):await h("adb",b(e,["shell","monkey","-p",n.value,"-c","android.intent.category.LAUNCHER","1"]))}async function I(e){e.booted||await N(e.id)}async function D(e,t){if("settings"===t.trim().toLowerCase())return void await h("adb",b(e,["shell","am","force-stop","com.android.settings"]));let n=await S(e,t);if("intent"===n.type)throw new p("INVALID_ARGS","Close requires a package name, not an intent");await h("adb",b(e,["shell","am","force-stop",n.value]))}async function O(e,t,n){await h("adb",b(e,["shell","input","tap",String(t),String(n)]))}async function x(e,t,n,i=800){await h("adb",b(e,["shell","input","swipe",String(t),String(n),String(t),String(n),String(i)]))}async function E(e,t){let n=t.replace(/ /g,"%s");await h("adb",b(e,["shell","input","text",n]))}async function _(e,t,n){await O(e,t,n)}async function k(e,t,n,i){await _(e,t,n),await E(e,i)}async function C(e,t,n=.6){let{width:i,height:r}=await M(e),a=Math.floor(i*n),o=Math.floor(r*n),s=Math.floor(i/2),l=Math.floor(r/2),c=s,u=l,d=s,f=l;switch(t){case"up":u=l-Math.floor(o/2),f=l+Math.floor(o/2);break;case"down":u=l+Math.floor(o/2),f=l-Math.floor(o/2);break;case"left":c=s-Math.floor(a/2),d=s+Math.floor(a/2);break;case"right":c=s+Math.floor(a/2),d=s-Math.floor(a/2);break;default:throw new p("INVALID_ARGS",`Unknown direction: ${t}`)}await h("adb",b(e,["shell","input","swipe",String(c),String(u),String(d),String(f),"300"]))}async function R(e,t){for(let n=0;n<8;n+=1){let n="";try{n=await F(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new p("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let n=t.toLowerCase(),i=/<node[^>]+>/g,r=i.exec(e);for(;r;){let t=r[0],a=/text="([^"]*)"/.exec(t),o=/content-desc="([^"]*)"/.exec(t),s=(a?.[1]??"").toLowerCase(),l=(o?.[1]??"").toLowerCase();if(s.includes(n)||l.includes(n)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),n=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((n+Number(e[4]))/2)}}return{x:0,y:0}}r=i.exec(e)}return null}(n,t))return;await C(e,"down",.5)}throw new p("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function P(e,t){let n=await h("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!n.stdoutBuffer)throw new p("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,n.stdoutBuffer)}async function T(e,t={}){return function(e,t,n){let i=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},n=[t],i=/<node\b[^>]*>|<\/node>/g,r=i.exec(e);for(;r;){let t=r[0];if(t.startsWith("</node")){n.length>1&&n.pop(),r=i.exec(e);continue}let a=function(e){let t=t=>{let n=RegExp(`${t}="([^"]*)"`).exec(e);return n?n[1]:null},n=e=>{let n=t(e);if(null!==n)return"true"===n};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:n("clickable"),enabled:n("enabled"),focusable:n("focusable")}}(t),o=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let n=Number(t[1]),i=Number(t[2]);return{x:n,y:i,width:Math.max(0,Number(t[3])-n),height:Math.max(0,Number(t[4])-i)}}(a.bounds),s=n[n.length-1],l={type:a.className,label:a.text||a.desc,value:a.text,identifier:a.resourceId,rect:o,enabled:a.enabled,hittable:a.clickable??a.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||n.push(l),r=i.exec(e)}return t}(e),r=[],a=!1,o=n.depth??1/0,s=n.scope?function(e,t){let n=t.toLowerCase(),i=[...e.children];for(;i.length>0;){let e=i.shift(),t=e.label?.toLowerCase()??"",r=e.value?.toLowerCase()??"",a=e.identifier?.toLowerCase()??"";if(t.includes(n)||r.includes(n)||a.includes(n))return e;i.push(...e.children)}return null}(i,n.scope):null,l=s?[s]:i.children,c=(e,t)=>{if(r.length>=800){a=!0;return}if(!(t>o)){for(let i of((n.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),n=!!(e.identifier&&e.identifier.trim().length>0);return t||n||!!e.hittable}return!0}(e,n))&&r.push({index:r.length,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:e.parentIndex}),e.children))if(c(i,t+1),a)return}};for(let e of l)if(c(e,0),a)break;return a?{nodes:r,truncated:a}:{nodes:r}}(await F(e),800,t)}async function L(){if(!await m("adb"))throw new p("TOOL_MISSING","adb not found in PATH")}async function M(e){let t=(await h("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new p("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function F(e){return await h("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await h("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}async function j(){if("darwin"!==process.platform)throw new p("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await m("xcrun"))throw new p("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await h("xcrun",["simctl","list","devices","-j"]);try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices))for(let n of t)n.isAvailable&&e.push({platform:"ios",id:n.udid,name:n.name,kind:"simulator",booted:"Booted"===n.state})}catch(e){throw new p("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await m("xcrun"))try{let t=await h("xcrun",["devicectl","list","devices","--json"]);for(let n of JSON.parse(t.stdout).devices??[])n.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:n.identifier,name:n.name,kind:"device",booted:!0})}catch{}return e}let U={settings:"com.apple.Preferences"};async function V(e,t){let n=t.trim();if(n.includes("."))return n;let i=U[n.toLowerCase()];if(i)return i;if("simulator"===e.kind){let i=(await Q(e)).filter(e=>e.name.toLowerCase()===n.toLowerCase());if(1===i.length)return i[0].bundleId;if(i.length>1)throw new p("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:i})}throw new p("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function $(e,t){let n=await V(e,t);if("simulator"===e.kind){await ee(e),await h("open",["-a","Simulator"],{allowFailure:!0}),await h("xcrun",["simctl","launch",e.id,n]);return}await h("xcrun",["devicectl","device","process","launch","--device",e.id,n])}async function B(e){"simulator"!==e.kind||"Booted"!==await et(e.id)&&(await ee(e),await h("open",["-a","Simulator"],{allowFailure:!0}))}async function G(e,t){let n=await V(e,t);if("simulator"===e.kind){await ee(e);let t=await h("xcrun",["simctl","terminate",e.id,n],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new p("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,n],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await h("xcrun",["devicectl","device","process","terminate","--device",e.id,n])}async function J(e,t,n){throw K(e,"press"),new p("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function W(e,t,n,i=800){throw K(e,"long-press"),new p("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function q(e,t,n){await J(e,t,n)}async function X(e,t){throw K(e,"type"),new p("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function z(e,t,n,i){await q(e,t,n),await X(e,i)}async function H(e,t,n=.6){throw K(e,"scroll"),new p("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function Y(e){throw new p("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function Z(e,t){if("simulator"===e.kind){await ee(e),await h("xcrun",["simctl","io",e.id,"screenshot",t]);return}await h("xcrun",["devicectl","device","screenshot","--device",e.id,t])}function K(e,t){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function Q(e){let t=(await h("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout;if(!t.trim().startsWith("{"))return[];try{let e=JSON.parse(t);return Object.entries(e).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e}))}catch{return[]}}async function ee(e){"simulator"!==e.kind||"Booted"!==await et(e.id)&&(await h("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await h("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function et(e){let t=await h("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let n=JSON.parse(t.stdout);for(let t of Object.values(n.devices??{})){let n=t.find(t=>t.udid===e);if(n)return n.state}}catch{}return null}let en=new Map;async function ei(e,t,n={}){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let i=await eo(e,n),r=await eu(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}catch(r){let i=r instanceof p?r:new p("COMMAND_FAILED",String(r));if("COMMAND_FAILED"===i.code&&"string"==typeof i.message&&i.message.includes("Runner did not accept connection")){await er(e.id);let i=await eo(e,n),r=await eu(e,i.port,t,n.logPath),a=await r.text(),o={};try{o=JSON.parse(a)}catch{throw new p("COMMAND_FAILED","Invalid runner response",{text:a})}if(!o.ok)throw new p("COMMAND_FAILED",o.error?.message??"Runner error",{runner:o,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:n.logPath});return o.data??{}}throw r}}async function er(e){let t=en.get(e);if(t){try{await eu(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}em(t.xctestrunPath),em(t.jsonPath),en.delete(e)}}async function ea(e){await h("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eo(e,t){let n=en.get(e.id);if(n)return n;await ea(e.id);let i=await es(e.id,t),r=await ef(),a=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"300",{xctestrunPath:s,jsonPath:l}=await eh(i,{AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a},`session-${e.id}-${r}`),c=o("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{ec(e,t.logPath,t.verbose)},onStderrChunk:e=>{ec(e,t.logPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(r),AGENT_DEVICE_RUNNER_TIMEOUT:a}}),u={device:e,deviceId:e.id,port:r,xctestrunPath:s,jsonPath:l,testPromise:c};return en.set(e.id,u),u}async function es(e,t){let n,i=a.join(d.homedir(),".agent-device","ios-runner"),r=a.join(i,"derived");if((n=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(n.toLowerCase()))try{u.rmSync(r,{recursive:!0,force:!0})}catch{}let s=el(r);if(s)return s;let l=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}(),f=a.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!u.existsSync(f))throw new p("COMMAND_FAILED","iOS runner project not found",{projectPath:f});try{await o("xcodebuild",["build-for-testing","-project",f,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",r],{onStdoutChunk:e=>{ec(e,t.logPath,t.verbose)},onStderrChunk:e=>{ec(e,t.logPath,t.verbose)}})}catch(n){let e=n instanceof p?n:new p("COMMAND_FAILED",String(n));throw new p("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let h=el(r);if(!h)throw new p("COMMAND_FAILED","Failed to locate .xctestrun after build");return h}function el(e){if(!u.existsSync(e))return null;let t=[],n=[e];for(;n.length>0;){let e=n.pop();for(let i of u.readdirSync(e,{withFileTypes:!0})){let r=a.join(e,i.name);if(i.isDirectory()){n.push(r);continue}if(i.isFile()&&i.name.endsWith(".xctestrun"))try{let e=u.statSync(r);t.push({path:r,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function ec(e,t,n){t&&u.appendFileSync(t,e),n&&process.stderr.write(e)}async function eu(e,t,n,i){i&&await ep(i,4e3);let r=Date.now(),a=null;for(;Date.now()-r<8e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let i=await ed(e.id,t,n);return new Response(i.body,{status:i.status})}let o=i?function(e){try{if(!u.existsSync(e))return null;let t=u.readFileSync(e,"utf8").match(/AGENT_DEVICE_RUNNER_PORT=(\d+)/);if(t)return Number(t[1])}catch{}return null}(i):null;if(o&&o!==t)try{return await fetch(`http://127.0.0.1:${o}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})}catch(e){a=e}throw new p("COMMAND_FAILED","Runner did not accept connection",{port:t,fallbackPort:o,logPath:i,lastError:a?String(a):void 0})}async function ed(e,t,n){let i=JSON.stringify(n),r=await h("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",i,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),a=r.stdout;if(0!==r.exitCode)throw new p("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:r.stdout,stderr:r.stderr,exitCode:r.exitCode});return{status:200,body:a}}async function ef(){return await new Promise((e,t)=>{let n=f.createServer();n.listen(0,"127.0.0.1",()=>{let i=n.address();n.close(),"object"==typeof i&&i?.port?e(i.port):t(new p("COMMAND_FAILED","Failed to allocate port"))}),n.on("error",t)})}async function ep(e,t){if(!u.existsSync(e))return;let n=Date.now(),i=0;for(;Date.now()-n<t;){if(!u.existsSync(e))return;let t=u.statSync(e);if(t.size>i){let n=u.openSync(e,"r"),r=Buffer.alloc(t.size-i);u.readSync(n,r,0,r.length,i),u.closeSync(n),i=t.size;let a=r.toString("utf8");if(a.includes("AGENT_DEVICE_RUNNER_LISTENER_READY")||a.includes("AGENT_DEVICE_RUNNER_PORT="))return}await new Promise(e=>setTimeout(e,100))}}async function eh(e,t,n){let i,r=a.dirname(e),o=n.replace(/[^a-zA-Z0-9._-]/g,"_"),s=a.join(r,`AgentDeviceRunner.env.${o}.json`),l=a.join(r,`AgentDeviceRunner.env.${o}.xctestrun`),c=await h("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new p("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{i=JSON.parse(c.stdout)}catch(t){throw new p("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let d=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},f=i.TestConfigurations;if(Array.isArray(f))for(let e of f){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&d(e)}for(let[e,t]of Object.entries(i))t&&"object"==typeof t&&t.TestBundlePath&&(d(t),i[e]=t);u.writeFileSync(s,JSON.stringify(i,null,2));let m=await h("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==m.exitCode)throw new p("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:m.stderr});return{xctestrunPath:l,jsonPath:s}}function em(e){try{u.existsSync(e)&&u.unlinkSync(e)}catch{}}async function ew(e){let t,n;if("ios"!==e.platform||"simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let i=await eg(),r=await h(i,[],{allowFailure:!0});if(0!==r.exitCode){let e=(r.stderr??"").toString(),t="";throw e.toLowerCase().includes("accessibility permission")&&(t=" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest)."),new p("COMMAND_FAILED","AX snapshot failed",{stderr:`${e}${t}`,stdout:r.stdout})}try{let e=JSON.parse(r.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");t=e.root,n=e.windowFrame??void 0}else t=e}catch(e){throw new p("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let a=t.frame??n,o=[],s=[],l=(e,t)=>{e.frame&&o.push(e.frame);let n=e.frame&&a?{x:e.frame.x-a.x,y:e.frame.y-a.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let i of(s.push({...e,frame:n,children:void 0,depth:t}),e.children??[]))l(i,t+1)};return l(t,0),{nodes:(function(e,t,n){if(!t||0===n.length)return e;let i=1/0,r=1/0;for(let e of n)e.x<i&&(i=e.x),e.y<r&&(r=e.y);return i<=5&&r<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(s,a,o).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth}))}}async function eg(){let e=function(){let e=process.cwd();for(let t=0;t<6;t+=1){let t=a.join(e,"package.json");if(u.existsSync(t))return e;e=a.dirname(e)}return process.cwd()}(),t=a.join(e,"ios-runner","AXSnapshot"),n=process.env.AGENT_DEVICE_AX_BINARY;if(n&&u.existsSync(n))return n;for(let t of[a.join(e,"bin","axsnapshot"),a.join(e,"dist","bin","axsnapshot"),a.join(e,"dist","axsnapshot")])if(u.existsSync(t))return t;let i=a.join(t,".build","release","axsnapshot");if(u.existsSync(i))return i;let r=await h("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==r.exitCode||!u.existsSync(i))throw new p("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:r.stderr,stdout:r.stdout});return i}async function ey(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await L();let e=await g();return await w(e,t)}if("ios"===t.platform){let e=await j();return await w(e,t)}let n=[];try{n.push(...await g())}catch{}try{n.push(...await j())}catch{}return await w(n,t)}async function eN(e,t,n,i,r){let a=function(e){switch(e.platform){case"android":return{open:t=>A(e,t),openDevice:()=>I(e),close:t=>D(e,t),tap:(t,n)=>O(e,t,n),longPress:(t,n,i)=>x(e,t,n,i),focus:(t,n)=>_(e,t,n),type:t=>E(e,t),fill:(t,n,i)=>k(e,t,n,i),scroll:(t,n)=>C(e,t,n),scrollIntoView:t=>R(e,t),screenshot:t=>P(e,t)};case"ios":return{open:t=>$(e,t),openDevice:()=>B(e),close:t=>G(e,t),tap:(t,n)=>J(e,t,n),longPress:(t,n,i)=>W(e,t,n,i),focus:(t,n)=>q(e,t,n),type:t=>X(e,t),fill:(t,n,i)=>z(e,t,n,i),scroll:(t,n)=>H(e,t,n),scrollIntoView:e=>Y(e),screenshot:t=>Z(e,t)};default:throw new p("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=n[0];if(!e)return await a.openDevice(),{app:null};return await a.open(e),{app:e}}case"close":{let e=n[0];if(!e)return{closed:"session"};return await a.close(e),{app:e}}case"press":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ei(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.tap(t,i),{x:t,y:i}}case"long-press":{let e=Number(n[0]),t=Number(n[1]),i=n[2]?Number(n[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new p("INVALID_ARGS","long-press requires x y [durationMs]");return await a.longPress(e,t,i),{x:e,y:t,durationMs:i}}case"focus":{let[t,i]=n.map(Number);if(Number.isNaN(t)||Number.isNaN(i))throw new p("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await ei(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.focus(t,i),{x:t,y:i}}case"type":{let t=n.join(" ");if(!t)throw new p("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await ei(e,{command:"type",text:t,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}):await a.type(t),{text:t}}case"fill":{let t=Number(n[0]),i=Number(n[1]),o=n.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(i)||!o)throw new p("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await ei(e,{command:"tap",x:t,y:i,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath}),await ei(e,{command:"type",text:o,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})):await a.fill(t,i,o),{x:t,y:i,text:o}}case"scroll":{let t=n[0],i=n[1]?Number(n[1]):void 0;if(!t)throw new p("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new p("INVALID_ARGS",`Unknown direction: ${t}`);let n=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await ei(e,{command:"swipe",direction:n,appBundleId:r?.appBundleId},{verbose:r?.verbose,logPath:r?.logPath})}else await a.scroll(t,i);return{direction:t,amount:i}}case"scrollintoview":{let e=n.join(" ");if(!e)throw new p("INVALID_ARGS","scrollintoview requires text");return await a.scrollIntoView(e),{text:e}}case"screenshot":{let e=i??`./screenshot-${Date.now()}.png`;return await a.screenshot(e),{path:e}}case"snapshot":{let t=r?.snapshotBackend??"ax";if("ios"===e.platform){if("simulator"!==e.kind)throw new p("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await ew(e)).nodes??[],truncated:!1,backend:"ax"};let n=await ei(e,{command:"snapshot",appBundleId:r?.appBundleId,interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw},{verbose:r?.verbose,logPath:r?.logPath});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"xctest"}}let n=await T(e,{interactiveOnly:r?.snapshotInteractiveOnly,compact:r?.snapshotCompact,depth:r?.snapshotDepth,scope:r?.snapshotScope,raw:r?.snapshotRaw});return{nodes:n.nodes??[],truncated:n.truncated??!1,backend:"android"}}default:throw new p("INVALID_ARGS",`Unknown command: ${t}`)}}function ev(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function eb(e,t){return e.find(e=>e.ref===t)??null}function eS(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}let eA=new Map,eI=a.join(d.homedir(),".agent-device"),eD=a.join(eI,"daemon.json"),eO=a.join(eI,"daemon.log"),ex=a.join(eI,"sessions"),eE=function(){try{let e=function(){let e=a.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=a.join(t,"package.json");if(u.existsSync(e))return t;t=a.dirname(t)}return e}();return JSON.parse(u.readFileSync(a.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),e_=n.randomBytes(24).toString("hex");function ek(e,t){return{appBundleId:t,verbose:e?.verbose,logPath:eO,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function eC(e){if(e.token!==e_)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,n=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(eA.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("open"===t){let i;if(eA.has(n))return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};let r=await ey(e.flags??{});await ej(r);let a=Array.from(eA.values()).find(e=>e.device.id===r.id);if(a)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${a.name}".`,details:{session:a.name,deviceId:r.id,deviceName:r.name}}};let o=e.positionals?.[0];if("ios"===r.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:V}));i=await t(r,e.positionals?.[0]??"")}catch{i=void 0}await eN(r,"open",e.positionals??[],e.flags?.out,{...ek(e.flags,i)});let s={name:n,device:r,createdAt:Date.now(),appBundleId:i,appName:o,actions:[]};return eR(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),eA.set(n,s),{ok:!0,data:{session:n}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{var i;let e=(i=t).startsWith("~/")?a.join(d.homedir(),i.slice(2)):a.resolve(i),r=JSON.parse(u.readFileSync(e,"utf8")),o=r.optimizedActions??r.actions??[];for(let e of o)e&&"replay"!==e.command&&await eC({token:e_,session:n,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:o.length,session:n}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let i=eA.get(n);return i?(e.positionals&&e.positionals.length>0&&await eN(i.device,"close",e.positionals??[],e.flags?.out,{...ek(e.flags,i.appBundleId)}),"ios"===i.device.platform&&"simulator"===i.device.kind&&await er(i.device.id),eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:n}}),eP(i),eA.delete(n),{ok:!0,data:{session:n}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let i=eA.get(n),r=i?.device??await ey(e.flags??{});i||await ej(r);let a=i?.appBundleId,o=await eN(r,"snapshot",[],e.flags?.out,{...ek(e.flags,a)}),s=(function(e){let t=[],n=[];for(let i of e){let e=i.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let r=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(i.type??"");if("group"===r||"ioscontentgroup"===r){t.push(e);continue}let a=Math.max(0,e-t.length);n.push({...i,depth:a})}return n})(o?.nodes??[]).map((e,t)=>({...e,ref:`e${t+1}`})),l={nodes:s,truncated:o?.truncated,createdAt:Date.now(),backend:o?.backend},c={name:n,device:r,createdAt:i?.createdAt??Date.now(),appBundleId:a,snapshot:l,actions:i?.actions??[]};return eR(c,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:s.length,truncated:o?.truncated??!1}}),eA.set(n,c),{ok:!0,data:{nodes:s,truncated:o?.truncated??!1,appName:i?.appName??a??r.name,appBundleId:a}}}if("click"===t){let i=eA.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=e.positionals?.[0]??"",a=ev(r);if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let o=eb(i.snapshot.nodes,a);if(!o?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(o=eL(i.snapshot.nodes,t))}if(!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found or has no bounds`}};let s=eM(o,i.snapshot.nodes),l=o.label?.trim();if("ios"===i.device.platform&&"simulator"===i.device.kind&&l&&function(e,t){let n=t.trim().toLowerCase();if(!n)return!1;let i=0;for(let t of e)if((t.label??"").trim().toLowerCase()===n&&(i+=1)>1)return!1;return 1===i}(i.snapshot.nodes,l))return await ei(i.device,{command:"tap",text:l,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eO}),eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,refLabel:l,mode:"text"}}),{ok:!0,data:{ref:a,mode:"text"}};let{x:c,y:u}=eS(o.rect);return await eN(i.device,"press",[String(c),String(u)],e.flags?.out,{...ek(e.flags,i.appBundleId)}),eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:a,x:c,y:u,refLabel:s}}),{ok:!0,data:{ref:a,x:c,y:u}}}if("fill"===t){let i=eA.get(n);if(e.positionals?.[0]?.startsWith("@")){if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=ev(e.positionals[0]);if(!n)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let r=e.positionals.length>=3?e.positionals[1]:"",a=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let o=eb(i.snapshot.nodes,n);if(!o?.rect&&r&&(o=eL(i.snapshot.nodes,r)),!o?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let s=eM(o,i.snapshot.nodes),{x:l,y:c}=eS(o.rect),u=await eN(i.device,"fill",[String(l),String(c),a],e.flags?.out,{...ek(e.flags,i.appBundleId)});return eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:u??{ref:n,x:l,y:c,refLabel:s}}),{ok:!0,data:u??{ref:n,x:l,y:c}}}}if("get"===t){let i=e.positionals?.[0],r=e.positionals?.[1];if("text"!==i&&"attrs"!==i)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let a=eA.get(n);if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let o=ev(r??"");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=eb(a.snapshot.nodes,o);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=eL(a.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found`}};if("attrs"===i)return eR(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o}}),{ok:!0,data:{ref:o,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return eR(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:o,text:l,node:s}}}if("rect"===t){let i=eA.get(n);if(!i?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=ev(e.positionals?.[0]??""),a="";if(r){let e=eb(i.snapshot.nodes,r);a=e?.label?.trim()??""}else a=e.positionals.join(" ").trim();if(!a)return{ok:!1,error:{code:"INVALID_ARGS",message:"rect requires a label or ref with label"}};if("ios"!==i.device.platform||"simulator"!==i.device.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"rect is only supported on iOS simulators"}};let o=await ei(i.device,{command:"rect",text:a,appBundleId:i.appBundleId},{verbose:e.flags?.verbose,logPath:eO});return eR(i,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{label:a,rect:o?.rect}}),{ok:!0,data:{label:a,rect:o?.rect}}}let r=eA.get(n);if(!r)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let o=await eN(r.device,t,e.positionals??[],e.flags?.out,{...ek(e.flags,r.appBundleId)});return eR(r,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:o??{}}),{ok:!0,data:o??{}}}function eR(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}=e;return{platform:t,device:n,udid:i,serial:r,out:a,verbose:o,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:u,snapshotRaw:d,snapshotBackend:f,noRecord:p,recordJson:h}}(t.flags),result:t.result})}function eP(e){try{u.existsSync(ex)||u.mkdirSync(ex,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),n=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),i=a.join(ex,`${t}-${n}.ad`),r=a.join(ex,`${t}-${n}.json`),o={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let n of e.actions)if("snapshot"!==n.command){if("click"===n.command||"fill"===n.command||"get"===n.command){let i=n.result?.refLabel;"string"==typeof i&&i.trim().length>0&&t.push({ts:n.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:i.trim()},result:{scope:i.trim()}})}t.push(n)}return t}(e)},s=function(e,t){let n=[],i=e.device.name.replace(/"/g,'\\"'),r=e.device.kind?` kind=${e.device.kind}`:"";for(let a of(n.push(`context platform=${e.device.platform} device="${i}"${r} theme=unknown`),t))a.flags?.noRecord||n.push(function(e){let t=[e.command];if("click"===e.command){let n=e.positionals?.[0];if(n){t.push(eT(n));let i=e.result?.refLabel;return"string"==typeof i&&i.trim().length>0&&t.push(eT(i)),t.join(" ")}}if("fill"===e.command){let n=e.positionals?.[0];if(n&&n.startsWith("@")){t.push(eT(n));let i=e.result?.refLabel,r=e.positionals.slice(1).join(" ");return"string"==typeof i&&i.trim().length>0&&t.push(eT(i)),r&&t.push(eT(r)),t.join(" ")}}if("get"===e.command){let n=e.positionals?.[0],i=e.positionals?.[1];if(n&&i){t.push(eT(n)),t.push(eT(i));let r=e.result?.refLabel;return"string"==typeof r&&r.trim().length>0&&t.push(eT(r)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",eT(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let n of e.positionals??[])t.push(eT(n));return t.join(" ")}(a));return`${n.join("\n")} | ||
| `}(e,o.optimizedActions);u.writeFileSync(i,s),e.actions.some(e=>e.flags?.recordJson)&&u.writeFileSync(r,JSON.stringify(o,null,2))}catch{}}function eT(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function eL(e,t){let n=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),i=(e.value??"").toLowerCase(),r=(e.identifier??"").toLowerCase();return t.includes(n)||i.includes(n)||r.includes(n)})??null}function eM(e,t){let n=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return n&&eF(n)?n:function(e,t){if(!e.rect)return;let n=e.rect.y+e.rect.height/2,i=null;for(let e of t){if(!e.rect)continue;let t=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);if(!t||!eF(t))continue;let r=Math.abs(e.rect.y+e.rect.height/2-n);(!i||r<i.distance)&&(i={label:t,distance:r})}return i?.label}(e,t)??(n&&eF(n)?n:void 0)}function eF(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}async function ej(e){if("ios"===e.platform&&"simulator"===e.kind){let{ensureBootedSimulator:t}=await Promise.resolve().then(()=>({ensureBootedSimulator:ee}));await t(e);return}if("android"===e.platform){let{waitForAndroidBoot:t}=await Promise.resolve().then(()=>({waitForAndroidBoot:N}));await t(e.id)}}(e=f.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async n=>{let i=(t+=n).indexOf("\n");for(;-1!==i;){let n,r=t.slice(0,i).trim();if(t=t.slice(i+1),0===r.length){i=t.indexOf("\n");continue}try{let e=JSON.parse(r);n=await eC(e)}catch(t){let e=l(t);n={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(n)} | ||
| `),i=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var n;n=t.port,u.existsSync(eI)||u.mkdirSync(eI,{recursive:!0}),u.writeFileSync(eO,""),u.writeFileSync(eD,JSON.stringify({port:n,token:e_,pid:process.pid,version:eE},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port} | ||
| `)}}),t=async()=>{for(let e of Array.from(eA.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await er(e.device.id),eP(e);e.close(()=>{u.existsSync(eD)&&u.unlinkSync(eD),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let n=e instanceof p?e:l(e);process.stderr.write(`Daemon error: ${n.message} | ||
| let e,t;import i from"node:crypto";import{isCancel as a,select as n}from"@clack/prompts";import{node_path as o,runCmdStreaming as r,promises as s,asAppError as l,fileURLToPath as c,runCmdBackground as d,node_fs as u,node_os as p,errors_AppError as f,runCmd as m,node_net as h,whichCmd as w}from"./861.js";async function g(e,t){let i=e,o=e=>e.toLowerCase().replace(/_/g," ").replace(/\s+/g," ").trim();if(t.platform&&(i=i.filter(e=>e.platform===t.platform)),t.udid){let e=i.find(e=>e.id===t.udid&&"ios"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No iOS device with UDID ${t.udid}`);return e}if(t.serial){let e=i.find(e=>e.id===t.serial&&"android"===e.platform);if(!e)throw new f("DEVICE_NOT_FOUND",`No Android device with serial ${t.serial}`);return e}if(t.deviceName){let e=o(t.deviceName),a=i.find(t=>o(t.name)===e);if(!a)throw new f("DEVICE_NOT_FOUND",`No device named ${t.deviceName}`);return a}if(1===i.length)return i[0];if(0===i.length)throw new f("DEVICE_NOT_FOUND","No devices found",{selector:t});let r=i.filter(e=>e.booted);if(1===r.length)return r[0];if(!process.env.CI&&process.stdin.isTTY&&process.stdout.isTTY){let e=await n({message:"Multiple devices available. Choose a device to continue:",options:(r.length>0?r:i).map(e=>({label:`${e.name} (${e.platform}${e.kind?`, ${e.kind}`:""}${e.booted?", booted":""})`,value:e.id}))});if(a(e))throw new f("INVALID_ARGS","Device selection cancelled");if(e){let t=i.find(t=>t.id===e);if(t)return t}}return r[0]??i[0]}async function y(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH");let e=(await m("adb",["devices","-l"])).stdout.split("\n").map(e=>e.trim()),t=[];for(let i of e){if(!i||i.startsWith("List of devices"))continue;let e=i.split(/\s+/),a=e[0];if("device"!==e[1])continue;let n=(e.find(e=>e.startsWith("model:"))??"").replace("model:","").replace(/_/g," ").trim()||a;if(a.startsWith("emulator-")){let e=await m("adb",["-s",a,"emu","avd","name"],{allowFailure:!0}),t=e.stdout.trim();0===e.exitCode&&t&&(n=t.replace(/_/g," "))}let o=await v(a);t.push({platform:"android",id:a,name:n,kind:a.startsWith("emulator-")?"emulator":"device",booted:o})}return t}async function v(e){try{let t=await m("adb",["-s",e,"shell","getprop","sys.boot_completed"],{allowFailure:!0});return"1"===t.stdout.trim()}catch{return!1}}async function N(e,t=6e4){let i=Date.now();for(;Date.now()-i<t;){if(await v(e))return;await new Promise(e=>setTimeout(e,1e3))}throw new f("COMMAND_FAILED","Android device did not finish booting in time",{serial:e,timeoutMs:t})}let I={settings:{type:"intent",value:"android.settings.SETTINGS"}};function b(e,t){return["-s",e.id,...t]}async function A(e,t){let i=t.trim();if(i.includes("."))return{type:"package",value:i};let a=I[i.toLowerCase()];if(a)return a;let n=(await m("adb",b(e,["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean).filter(e=>e.toLowerCase().includes(i.toLowerCase()));if(1===n.length)return{type:"package",value:n[0]};if(n.length>1)throw new f("INVALID_ARGS",`Multiple packages matched "${t}"`,{matches:n});throw new f("APP_NOT_INSTALLED",`No package found matching "${t}"`)}async function S(e,t="launchable"){if("launchable"===t){let t=await m("adb",b(e,["shell","cmd","package","query-activities","--brief","-a","android.intent.action.MAIN","-c","android.intent.category.LAUNCHER"]),{allowFailure:!0});if(0===t.exitCode&&t.stdout.trim().length>0){let e=new Set;for(let i of t.stdout.split("\n")){let t=i.trim();if(!t)continue;let a=t.split(/\s+/)[0],n=a.includes("/")?a.split("/")[0]:a;n&&e.add(n)}if(e.size>0)return Array.from(e)}}return(await m("adb",b(e,"user-installed"===t?["shell","pm","list","packages","-3"]:["shell","pm","list","packages"]))).stdout.split("\n").map(e=>e.replace("package:","").trim()).filter(Boolean)}async function D(e,t){e.booted||await N(e.id);let i=await A(e,t);"intent"===i.type?await m("adb",b(e,["shell","am","start","-a",i.value])):await m("adb",b(e,["shell","monkey","-p",i.value,"-c","android.intent.category.LAUNCHER","1"]))}async function O(e){e.booted||await N(e.id)}async function k(e,t){if("settings"===t.trim().toLowerCase())return void await m("adb",b(e,["shell","am","force-stop","com.android.settings"]));let i=await A(e,t);if("intent"===i.type)throw new f("INVALID_ARGS","Close requires a package name, not an intent");await m("adb",b(e,["shell","am","force-stop",i.value]))}async function x(e,t,i){await m("adb",b(e,["shell","input","tap",String(t),String(i)]))}async function _(e){await m("adb",b(e,["shell","input","keyevent","4"]))}async function E(e){await m("adb",b(e,["shell","input","keyevent","3"]))}async function P(e){await m("adb",b(e,["shell","input","keyevent","187"]))}async function R(e,t,i,a=800){await m("adb",b(e,["shell","input","swipe",String(t),String(i),String(t),String(i),String(a)]))}async function C(e,t){let i=t.replace(/ /g,"%s");await m("adb",b(e,["shell","input","text",i]))}async function T(e,t,i){await x(e,t,i)}async function L(e,t,i,a){await T(e,t,i),await C(e,a)}async function M(e,t,i=.6){let{width:a,height:n}=await V(e),o=Math.floor(a*i),r=Math.floor(n*i),s=Math.floor(a/2),l=Math.floor(n/2),c=s,d=l,u=s,p=l;switch(t){case"up":d=l-Math.floor(r/2),p=l+Math.floor(r/2);break;case"down":d=l+Math.floor(r/2),p=l-Math.floor(r/2);break;case"left":c=s-Math.floor(o/2),u=s+Math.floor(o/2);break;case"right":c=s+Math.floor(o/2),u=s-Math.floor(o/2);break;default:throw new f("INVALID_ARGS",`Unknown direction: ${t}`)}await m("adb",b(e,["shell","input","swipe",String(c),String(d),String(u),String(p),"300"]))}async function F(e,t){for(let i=0;i<8;i+=1){let i="";try{i=await B(e)}catch(t){let e=t instanceof Error?t.message:String(t);throw new f("UNSUPPORTED_OPERATION",`uiautomator dump failed: ${e}`)}if(function(e,t){let i=t.toLowerCase(),a=/<node[^>]+>/g,n=a.exec(e);for(;n;){let t=n[0],o=/text="([^"]*)"/.exec(t),r=/content-desc="([^"]*)"/.exec(t),s=(o?.[1]??"").toLowerCase(),l=(r?.[1]??"").toLowerCase();if(s.includes(i)||l.includes(i)){let e=/bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"/.exec(t);if(e){let t=Number(e[1]),i=Number(e[2]);return{x:Math.floor((t+Number(e[3]))/2),y:Math.floor((i+Number(e[4]))/2)}}return{x:0,y:0}}n=a.exec(e)}return null}(i,t))return;await M(e,"down",.5)}throw new f("COMMAND_FAILED",`Could not find element containing "${t}" after scrolling`)}async function U(e,t){let i=await m("adb",b(e,["exec-out","screencap","-p"]),{binaryStdout:!0});if(!i.stdoutBuffer)throw new f("COMMAND_FAILED","Failed to capture screenshot");await s.writeFile(t,i.stdoutBuffer)}async function j(e,t={}){return function(e,t,i){let a=function(e){let t={type:null,label:null,value:null,identifier:null,depth:-1,children:[]},i=[t],a=/<node\b[^>]*>|<\/node>/g,n=a.exec(e);for(;n;){let t=n[0];if(t.startsWith("</node")){i.length>1&&i.pop(),n=a.exec(e);continue}let o=function(e){let t=t=>{let i=RegExp(`${t}="([^"]*)"`).exec(e);return i?i[1]:null},i=e=>{let i=t(e);if(null!==i)return"true"===i};return{text:t("text"),desc:t("content-desc"),resourceId:t("resource-id"),className:t("class"),bounds:t("bounds"),clickable:i("clickable"),enabled:i("enabled"),focusable:i("focusable")}}(t),r=function(e){if(!e)return;let t=/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(e);if(!t)return;let i=Number(t[1]),a=Number(t[2]);return{x:i,y:a,width:Math.max(0,Number(t[3])-i),height:Math.max(0,Number(t[4])-a)}}(o.bounds),s=i[i.length-1],l={type:o.className,label:o.text||o.desc,value:o.text,identifier:o.resourceId,rect:r,enabled:o.enabled,hittable:o.clickable??o.focusable,depth:s.depth+1,parentIndex:void 0,children:[]};s.children.push(l),t.endsWith("/>")||i.push(l),n=a.exec(e)}return t}(e),n=[],o=!1,r=i.depth??1/0,s=i.scope?function(e,t){let i=t.toLowerCase(),a=[...e.children];for(;a.length>0;){let e=a.shift(),t=e.label?.toLowerCase()??"",n=e.value?.toLowerCase()??"",o=e.identifier?.toLowerCase()??"";if(t.includes(i)||n.includes(i)||o.includes(i))return e;a.push(...e.children)}return null}(a,i.scope):null,l=s?[s]:a.children,c=(e,t)=>{if(n.length>=800){o=!0;return}if(!(t>r)){for(let a of((i.raw||function(e,t){if(t.interactiveOnly)return!!e.hittable;if(t.compact){let t=!!(e.label&&e.label.trim().length>0),i=!!(e.identifier&&e.identifier.trim().length>0);return t||i||!!e.hittable}return!0}(e,i))&&n.push({index:n.length,type:e.type??void 0,label:e.label??void 0,value:e.value??void 0,identifier:e.identifier??void 0,rect:e.rect,enabled:e.enabled,hittable:e.hittable,depth:t,parentIndex:e.parentIndex}),e.children))if(c(a,t+1),o)return}};for(let e of l)if(c(e,0),o)break;return o?{nodes:n,truncated:o}:{nodes:n}}(await B(e),800,t)}async function $(){if(!await w("adb"))throw new f("TOOL_MISSING","adb not found in PATH")}async function V(e){let t=(await m("adb",b(e,["shell","wm","size"]))).stdout.match(/Physical size:\s*(\d+)x(\d+)/);if(!t)throw new f("COMMAND_FAILED","Unable to read screen size");return{width:Number(t[1]),height:Number(t[2])}}async function B(e){return await m("adb",b(e,["shell","uiautomator","dump","/sdcard/window_dump.xml"])),(await m("adb",b(e,["shell","cat","/sdcard/window_dump.xml"]))).stdout}async function G(){if("darwin"!==process.platform)throw new f("UNSUPPORTED_PLATFORM","iOS tools are only available on macOS");if(!await w("xcrun"))throw new f("TOOL_MISSING","xcrun not found in PATH");let e=[],t=await m("xcrun",["simctl","list","devices","-j"]);try{let i=JSON.parse(t.stdout);for(let t of Object.values(i.devices))for(let i of t)i.isAvailable&&e.push({platform:"ios",id:i.udid,name:i.name,kind:"simulator",booted:"Booted"===i.state})}catch(e){throw new f("COMMAND_FAILED","Failed to parse simctl devices JSON",void 0,e)}if(await w("xcrun"))try{let t=await m("xcrun",["devicectl","list","devices","--json"]);for(let i of JSON.parse(t.stdout).devices??[])i.platform?.toLowerCase().includes("ios")&&e.push({platform:"ios",id:i.identifier,name:i.name,kind:"device",booted:!0})}catch{}return e}let J={settings:"com.apple.Preferences"};async function q(e,t){let i=t.trim();if(i.includes("."))return i;let a=J[i.toLowerCase()];if(a)return a;if("simulator"===e.kind){let a=(await en(e)).filter(e=>e.name.toLowerCase()===i.toLowerCase());if(1===a.length)return a[0].bundleId;if(a.length>1)throw new f("INVALID_ARGS",`Multiple apps matched "${t}"`,{matches:a})}throw new f("APP_NOT_INSTALLED",`No app found matching "${t}"`)}async function W(e,t){let i=await q(e,t);if("simulator"===e.kind){await eo(e),await m("open",["-a","Simulator"],{allowFailure:!0}),await m("xcrun",["simctl","launch",e.id,i]);return}await m("xcrun",["devicectl","device","process","launch","--device",e.id,i])}async function X(e){"simulator"!==e.kind||"Booted"!==await er(e.id)&&(await eo(e),await m("open",["-a","Simulator"],{allowFailure:!0}))}async function z(e,t){let i=await q(e,t);if("simulator"===e.kind){await eo(e);let t=await m("xcrun",["simctl","terminate",e.id,i],{allowFailure:!0});if(0!==t.exitCode){if(t.stderr.toLowerCase().includes("found nothing to terminate"))return;throw new f("COMMAND_FAILED",`xcrun exited with code ${t.exitCode}`,{cmd:"xcrun",args:["simctl","terminate",e.id,i],stdout:t.stdout,stderr:t.stderr,exitCode:t.exitCode})}return}await m("xcrun",["devicectl","device","process","terminate","--device",e.id,i])}async function H(e,t,i){throw ea(e,"press"),new f("UNSUPPORTED_OPERATION","simctl io tap is not available; use the XCTest runner for input")}async function Y(e,t,i,a=800){throw ea(e,"long-press"),new f("UNSUPPORTED_OPERATION","long-press is not supported on iOS simulators without XCTest runner support")}async function Z(e,t,i){await H(e,t,i)}async function K(e,t){throw ea(e,"type"),new f("UNSUPPORTED_OPERATION","simctl io keyboard is not available; use the XCTest runner for input")}async function Q(e,t,i,a){await Z(e,t,i),await K(e,a)}async function ee(e,t,i=.6){throw ea(e,"scroll"),new f("UNSUPPORTED_OPERATION","simctl io swipe is not available; use the XCTest runner for input")}async function et(e){throw new f("UNSUPPORTED_OPERATION",`scrollintoview is not supported on iOS without UI automation (${e})`)}async function ei(e,t){if("simulator"===e.kind){await eo(e),await m("xcrun",["simctl","io",e.id,"screenshot",t]);return}await m("xcrun",["devicectl","device","screenshot","--device",e.id,t])}function ea(e,t){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION",`${t} is only supported on iOS simulators in v1`)}async function en(e){let t=(await m("xcrun",["simctl","listapps",e.id],{allowFailure:!0})).stdout.trim();if(!t)return[];let i=null;if(t.startsWith("{"))try{i=JSON.parse(t)}catch{i=null}if(!i&&t.startsWith("{"))try{let e=await m("plutil",["-convert","json","-o","-","-"],{allowFailure:!0,stdin:t});0===e.exitCode&&e.stdout.trim().startsWith("{")&&(i=JSON.parse(e.stdout))}catch{i=null}return i?Object.entries(i).map(([e,t])=>({bundleId:e,name:t.CFBundleDisplayName??t.CFBundleName??e})):[]}async function eo(e){"simulator"!==e.kind||"Booted"!==await er(e.id)&&(await m("xcrun",["simctl","boot",e.id],{allowFailure:!0}),await m("xcrun",["simctl","bootstatus",e.id,"-b"],{allowFailure:!0}))}async function er(e){let t=await m("xcrun",["simctl","list","devices","-j"],{allowFailure:!0});if(0!==t.exitCode)return null;try{let i=JSON.parse(t.stdout);for(let t of Object.values(i.devices??{})){let i=t.find(t=>t.udid===e);if(i)return i.state}}catch{}return null}let es=new Map;async function el(e,t,i={}){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","iOS runner only supports simulators in v1");try{let a=await eu(e,i),n=await eh(e,a.port,t,i.logPath),o=await n.text(),r={};try{r=JSON.parse(o)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:o})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:i.logPath});return r.data??{}}catch(n){let a=n instanceof f?n:new f("COMMAND_FAILED",String(n));if("COMMAND_FAILED"===a.code&&"string"==typeof a.message&&a.message.includes("Runner did not accept connection")){await ec(e.id);let a=await eu(e,i),n=await eh(e,a.port,t,i.logPath),o=await n.text(),r={};try{r=JSON.parse(o)}catch{throw new f("COMMAND_FAILED","Invalid runner response",{text:o})}if(!r.ok)throw new f("COMMAND_FAILED",r.error?.message??"Runner error",{runner:r,xcodebuild:{exitCode:1,stdout:"",stderr:""},logPath:i.logPath});return r.data??{}}throw n}}async function ec(e){let t=es.get(e);if(t){try{await eh(t.device,t.port,{command:"shutdown"})}catch{}try{await t.testPromise}catch{}eN(t.xctestrunPath),eN(t.jsonPath),es.delete(e)}}async function ed(e){await m("xcrun",["simctl","bootstatus",e,"-b"],{allowFailure:!0})}async function eu(e,t){let i=es.get(e.id);if(i)return i;await ed(e.id);let a=await ep(e.id,t),n=await eg(),o=process.env.AGENT_DEVICE_RUNNER_TIMEOUT??"300",{xctestrunPath:s,jsonPath:l}=await ev(a,{AGENT_DEVICE_RUNNER_PORT:String(n),AGENT_DEVICE_RUNNER_TIMEOUT:o},`session-${e.id}-${n}`),c=r("xcodebuild",["test-without-building","-only-testing","AgentDeviceRunnerUITests/RunnerTests/testCommand","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-xctestrun",s,"-destination",`platform=iOS Simulator,id=${e.id}`],{onStdoutChunk:e=>{em(e,t.logPath,t.verbose)},onStderrChunk:e=>{em(e,t.logPath,t.verbose)},allowFailure:!0,env:{...process.env,AGENT_DEVICE_RUNNER_PORT:String(n),AGENT_DEVICE_RUNNER_TIMEOUT:o}}),d={device:e,deviceId:e.id,port:n,xctestrunPath:s,jsonPath:l,testPromise:c};return es.set(e.id,d),d}async function ep(e,t){let i,a=o.join(p.homedir(),".agent-device","ios-runner"),n=o.join(a,"derived");if((i=process.env.AGENT_DEVICE_IOS_CLEAN_DERIVED)&&["1","true","yes","on"].includes(i.toLowerCase()))try{u.rmSync(n,{recursive:!0,force:!0})}catch{}let s=ef(n);if(s)return s;let l=function(){let e=o.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=o.join(t,"package.json");if(u.existsSync(e))return t;t=o.dirname(t)}return e}(),d=o.join(l,"ios-runner","AgentDeviceRunner","AgentDeviceRunner.xcodeproj");if(!u.existsSync(d))throw new f("COMMAND_FAILED","iOS runner project not found",{projectPath:d});try{await r("xcodebuild",["build-for-testing","-project",d,"-scheme","AgentDeviceRunner","-parallel-testing-enabled","NO","-maximum-concurrent-test-simulator-destinations","1","-destination",`platform=iOS Simulator,id=${e}`,"-derivedDataPath",n],{onStdoutChunk:e=>{em(e,t.logPath,t.verbose)},onStderrChunk:e=>{em(e,t.logPath,t.verbose)}})}catch(i){let e=i instanceof f?i:new f("COMMAND_FAILED",String(i));throw new f("COMMAND_FAILED","xcodebuild build-for-testing failed",{error:e.message,details:e.details,logPath:t.logPath})}let m=ef(n);if(!m)throw new f("COMMAND_FAILED","Failed to locate .xctestrun after build");return m}function ef(e){if(!u.existsSync(e))return null;let t=[],i=[e];for(;i.length>0;){let e=i.pop();for(let a of u.readdirSync(e,{withFileTypes:!0})){let n=o.join(e,a.name);if(a.isDirectory()){i.push(n);continue}if(a.isFile()&&a.name.endsWith(".xctestrun"))try{let e=u.statSync(n);t.push({path:n,mtimeMs:e.mtimeMs})}catch{}}}return 0===t.length?null:(t.sort((e,t)=>t.mtimeMs-e.mtimeMs),t[0]?.path??null)}function em(e,t,i){t&&u.appendFileSync(t,e),i&&process.stderr.write(e)}async function eh(e,t,i,a){a&&await ey(a,4e3);let n=Date.now(),o=null;for(;Date.now()-n<8e3;)try{return await fetch(`http://127.0.0.1:${t}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)})}catch(e){o=e,await new Promise(e=>setTimeout(e,100))}if("simulator"===e.kind){let a=await ew(e.id,t,i);return new Response(a.body,{status:a.status})}let r=a?function(e){try{if(!u.existsSync(e))return null;let t=u.readFileSync(e,"utf8").match(/AGENT_DEVICE_RUNNER_PORT=(\d+)/);if(t)return Number(t[1])}catch{}return null}(a):null;if(r&&r!==t)try{return await fetch(`http://127.0.0.1:${r}/command`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)})}catch(e){o=e}throw new f("COMMAND_FAILED","Runner did not accept connection",{port:t,fallbackPort:r,logPath:a,lastError:o?String(o):void 0})}async function ew(e,t,i){let a=JSON.stringify(i),n=await m("xcrun",["simctl","spawn",e,"/usr/bin/curl","-s","-X","POST","-H","Content-Type: application/json","--data",a,`http://127.0.0.1:${t}/command`],{allowFailure:!0}),o=n.stdout;if(0!==n.exitCode)throw new f("COMMAND_FAILED","Runner did not accept connection (simctl spawn)",{port:t,stdout:n.stdout,stderr:n.stderr,exitCode:n.exitCode});return{status:200,body:o}}async function eg(){return await new Promise((e,t)=>{let i=h.createServer();i.listen(0,"127.0.0.1",()=>{let a=i.address();i.close(),"object"==typeof a&&a?.port?e(a.port):t(new f("COMMAND_FAILED","Failed to allocate port"))}),i.on("error",t)})}async function ey(e,t){if(!u.existsSync(e))return;let i=Date.now(),a=0;for(;Date.now()-i<t;){if(!u.existsSync(e))return;let t=u.statSync(e);if(t.size>a){let i=u.openSync(e,"r"),n=Buffer.alloc(t.size-a);u.readSync(i,n,0,n.length,a),u.closeSync(i),a=t.size;let o=n.toString("utf8");if(o.includes("AGENT_DEVICE_RUNNER_LISTENER_READY")||o.includes("AGENT_DEVICE_RUNNER_PORT="))return}await new Promise(e=>setTimeout(e,100))}}async function ev(e,t,i){let a,n=o.dirname(e),r=i.replace(/[^a-zA-Z0-9._-]/g,"_"),s=o.join(n,`AgentDeviceRunner.env.${r}.json`),l=o.join(n,`AgentDeviceRunner.env.${r}.xctestrun`),c=await m("plutil",["-convert","json","-o","-",e],{allowFailure:!0});if(0!==c.exitCode||!c.stdout.trim())throw new f("COMMAND_FAILED","Failed to read xctestrun plist",{xctestrunPath:e,stderr:c.stderr});try{a=JSON.parse(c.stdout)}catch(t){throw new f("COMMAND_FAILED","Failed to parse xctestrun JSON",{xctestrunPath:e,error:String(t)})}let d=e=>{e.EnvironmentVariables={...e.EnvironmentVariables??{},...t},e.UITestEnvironmentVariables={...e.UITestEnvironmentVariables??{},...t},e.UITargetAppEnvironmentVariables={...e.UITargetAppEnvironmentVariables??{},...t},e.TestingEnvironmentVariables={...e.TestingEnvironmentVariables??{},...t}},p=a.TestConfigurations;if(Array.isArray(p))for(let e of p){if(!e||"object"!=typeof e)continue;let t=e.TestTargets;if(Array.isArray(t))for(let e of t)e&&"object"==typeof e&&d(e)}for(let[e,t]of Object.entries(a))t&&"object"==typeof t&&t.TestBundlePath&&(d(t),a[e]=t);u.writeFileSync(s,JSON.stringify(a,null,2));let h=await m("plutil",["-convert","xml1","-o",l,s],{allowFailure:!0});if(0!==h.exitCode)throw new f("COMMAND_FAILED","Failed to write xctestrun plist",{tmpXctestrunPath:l,stderr:h.stderr});return{xctestrunPath:l,jsonPath:s}}function eN(e){try{u.existsSync(e)&&u.unlinkSync(e)}catch{}}async function eI(e){let t,i;if("ios"!==e.platform||"simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","AX snapshot is only supported on iOS simulators");let a=await eb(),n=await m(a,[],{allowFailure:!0});if(0!==n.exitCode){let e=(n.stderr??"").toString(),t="";throw e.toLowerCase().includes("accessibility permission")&&(t=" Enable Accessibility for your terminal in System Settings > Privacy & Security > Accessibility, or use --backend xctest (slower snapshots via XCTest)."),e.toLowerCase().includes("could not find ios app content")&&(t=" AX snapshot sometimes caches empty content. Try restarting the Simulator app."),new f("COMMAND_FAILED","AX snapshot failed",{stderr:`${e}${t}`,stdout:n.stdout})}try{let e=JSON.parse(n.stdout);if(e&&"object"==typeof e&&"root"in e){if(!e.root)throw Error("AX snapshot missing root");t=e.root,i=e.windowFrame??void 0}else t=e}catch(e){throw new f("COMMAND_FAILED","Invalid AX snapshot JSON",{error:String(e)})}let o=t.frame??i,r=[],s=[],l=(e,t)=>{e.frame&&r.push(e.frame);let i=e.frame&&o?{x:e.frame.x-o.x,y:e.frame.y-o.y,width:e.frame.width,height:e.frame.height}:e.frame;for(let a of(s.push({...e,frame:i,children:void 0,depth:t}),e.children??[]))l(a,t+1)};return l(t,0),{nodes:(function(e,t,i){if(!t||0===i.length)return e;let a=1/0,n=1/0;for(let e of i)e.x<a&&(a=e.x),e.y<n&&(n=e.y);return a<=5&&n<=5?e.map(e=>({...e,frame:e.frame?{x:e.frame.x+t.x,y:e.frame.y+t.y,width:e.frame.width,height:e.frame.height}:void 0})):e})(s,o,r).map((e,t)=>({index:t,type:e.subrole??e.role,label:e.label,value:e.value,identifier:e.identifier,rect:e.frame?{x:e.frame.x,y:e.frame.y,width:e.frame.width,height:e.frame.height}:void 0,depth:e.depth}))}}async function eb(){let e=function(){let e=process.cwd();for(let t=0;t<6;t+=1){let t=o.join(e,"package.json");if(u.existsSync(t))return e;e=o.dirname(e)}return process.cwd()}(),t=o.join(e,"ios-runner","AXSnapshot"),i=process.env.AGENT_DEVICE_AX_BINARY;if(i&&u.existsSync(i))return i;let a=o.join(e,"dist","bin","axsnapshot");if(u.existsSync(a))return a;let n=o.join(t,".build","release","axsnapshot");if(u.existsSync(n))return n;let r=await m("swift",["build","-c","release"],{cwd:t,allowFailure:!0});if(0!==r.exitCode||!u.existsSync(n))throw new f("COMMAND_FAILED","Failed to build AX snapshot tool",{stderr:r.stderr,stdout:r.stdout});return n}async function eA(e){let t={platform:e.platform,deviceName:e.device,udid:e.udid,serial:e.serial};if("android"===t.platform){await $();let e=await y();return await g(e,t)}if("ios"===t.platform){let e=await G();return await g(e,t)}let i=[];try{i.push(...await y())}catch{}try{i.push(...await G())}catch{}return await g(i,t)}async function eS(e,t,i,a,n){let o=function(e){switch(e.platform){case"android":return{open:t=>D(e,t),openDevice:()=>O(e),close:t=>k(e,t),tap:(t,i)=>x(e,t,i),longPress:(t,i,a)=>R(e,t,i,a),focus:(t,i)=>T(e,t,i),type:t=>C(e,t),fill:(t,i,a)=>L(e,t,i,a),scroll:(t,i)=>M(e,t,i),scrollIntoView:t=>F(e,t),screenshot:t=>U(e,t)};case"ios":return{open:t=>W(e,t),openDevice:()=>X(e),close:t=>z(e,t),tap:(t,i)=>H(e,t,i),longPress:(t,i,a)=>Y(e,t,i,a),focus:(t,i)=>Z(e,t,i),type:t=>K(e,t),fill:(t,i,a)=>Q(e,t,i,a),scroll:(t,i)=>ee(e,t,i),scrollIntoView:e=>et(e),screenshot:t=>ei(e,t)};default:throw new f("UNSUPPORTED_PLATFORM",`Unsupported platform: ${e.platform}`)}}(e);switch(t){case"open":{let e=i[0];if(!e)return await o.openDevice(),{app:null};return await o.open(e),{app:e}}case"close":{let e=i[0];if(!e)return{closed:"session"};return await o.close(e),{app:e}}case"press":{let[t,a]=i.map(Number);if(Number.isNaN(t)||Number.isNaN(a))throw new f("INVALID_ARGS","press requires x y");return"ios"===e.platform&&"simulator"===e.kind?await el(e,{command:"tap",x:t,y:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}):await o.tap(t,a),{x:t,y:a}}case"long-press":{let e=Number(i[0]),t=Number(i[1]),a=i[2]?Number(i[2]):void 0;if(Number.isNaN(e)||Number.isNaN(t))throw new f("INVALID_ARGS","long-press requires x y [durationMs]");return await o.longPress(e,t,a),{x:e,y:t,durationMs:a}}case"focus":{let[t,a]=i.map(Number);if(Number.isNaN(t)||Number.isNaN(a))throw new f("INVALID_ARGS","focus requires x y");return"ios"===e.platform&&"simulator"===e.kind?await el(e,{command:"tap",x:t,y:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}):await o.focus(t,a),{x:t,y:a}}case"type":{let t=i.join(" ");if(!t)throw new f("INVALID_ARGS","type requires text");return"ios"===e.platform&&"simulator"===e.kind?await el(e,{command:"type",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}):await o.type(t),{text:t}}case"fill":{let t=Number(i[0]),a=Number(i[1]),r=i.slice(2).join(" ");if(Number.isNaN(t)||Number.isNaN(a)||!r)throw new f("INVALID_ARGS","fill requires x y text");return"ios"===e.platform&&"simulator"===e.kind?(await el(e,{command:"tap",x:t,y:a,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),await el(e,{command:"type",text:r,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath})):await o.fill(t,a,r),{x:t,y:a,text:r}}case"scroll":{let t=i[0],a=i[1]?Number(i[1]):void 0;if(!t)throw new f("INVALID_ARGS","scroll requires direction");if("ios"===e.platform&&"simulator"===e.kind){if(!["up","down","left","right"].includes(t))throw new f("INVALID_ARGS",`Unknown direction: ${t}`);let i=function(e){switch(e){case"up":return"down";case"down":return"up";case"left":return"right";case"right":return"left"}}(t);await el(e,{command:"swipe",direction:i,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath})}else await o.scroll(t,a);return{direction:t,amount:a}}case"scrollintoview":{let t=i.join(" ").trim();if(!t)throw new f("INVALID_ARGS","scrollintoview requires text");if("ios"===e.platform&&"simulator"===e.kind){for(let i=0;i<8;i+=1){let a=await el(e,{command:"findText",text:t,appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath});if(a?.found)return{text:t,attempts:i+1};await el(e,{command:"swipe",direction:"up",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),await new Promise(e=>setTimeout(e,300))}throw new f("COMMAND_FAILED",`scrollintoview could not find text: ${t}`)}return await o.scrollIntoView(t),{text:t}}case"screenshot":{let e=a??`./screenshot-${Date.now()}.png`;return await o.screenshot(e),{path:e}}case"back":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","back is only supported on iOS simulators in v1");return await el(e,{command:"back",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),{action:"back"}}return await _(e),{action:"back"};case"home":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","home is only supported on iOS simulators in v1");return await el(e,{command:"home",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),{action:"home"}}return await E(e),{action:"home"};case"app-switcher":if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","app-switcher is only supported on iOS simulators in v1");return await el(e,{command:"appSwitcher",appBundleId:n?.appBundleId},{verbose:n?.verbose,logPath:n?.logPath}),{action:"app-switcher"}}return await P(e),{action:"app-switcher"};case"snapshot":{let t=n?.snapshotBackend??"ax";if("ios"===e.platform){if("simulator"!==e.kind)throw new f("UNSUPPORTED_OPERATION","snapshot is only supported on iOS simulators in v1");if("ax"===t)return{nodes:(await eI(e)).nodes??[],truncated:!1,backend:"ax"};let i=await el(e,{command:"snapshot",appBundleId:n?.appBundleId,interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw},{verbose:n?.verbose,logPath:n?.logPath});return{nodes:i.nodes??[],truncated:i.truncated??!1,backend:"xctest"}}let i=await j(e,{interactiveOnly:n?.snapshotInteractiveOnly,compact:n?.snapshotCompact,depth:n?.snapshotDepth,scope:n?.snapshotScope,raw:n?.snapshotRaw});return{nodes:i.nodes??[],truncated:i.truncated??!1,backend:"android"}}default:throw new f("INVALID_ARGS",`Unknown command: ${t}`)}}function eD(e){return e.map((e,t)=>({...e,ref:`e${t+1}`}))}function eO(e){let t=e.trim();return t.startsWith("@")?t.slice(1)||null:t.startsWith("e")?t:null}function ek(e,t){return e.find(e=>e.ref===t)??null}function ex(e){return{x:Math.round(e.x+e.width/2),y:Math.round(e.y+e.height/2)}}let e_=new Map,eE=o.join(p.homedir(),".agent-device"),eP=o.join(eE,"daemon.json"),eR=o.join(eE,"daemon.log"),eC=o.join(eE,"sessions"),eT=function(){try{let e=function(){let e=o.dirname(c(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=o.join(t,"package.json");if(u.existsSync(e))return t;t=o.dirname(t)}return e}();return JSON.parse(u.readFileSync(o.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}(),eL=i.randomBytes(24).toString("hex");function eM(e,t){return{appBundleId:t,verbose:e?.verbose,logPath:eR,snapshotInteractiveOnly:e?.snapshotInteractiveOnly,snapshotCompact:e?.snapshotCompact,snapshotDepth:e?.snapshotDepth,snapshotScope:e?.snapshotScope,snapshotRaw:e?.snapshotRaw,snapshotBackend:e?.snapshotBackend}}async function eF(e){if(e.token!==eL)return{ok:!1,error:{code:"UNAUTHORIZED",message:"Invalid token"}};let t=e.command,i=e.session||"default";if("session_list"===t)return{ok:!0,data:{sessions:Array.from(e_.values()).map(e=>({name:e.name,platform:e.device.platform,device:e.device.name,id:e.device.id,createdAt:e.createdAt}))}};if("devices"===t)try{let t=[];if(e.flags?.platform==="android"){let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:y}));t.push(...await e())}else if(e.flags?.platform==="ios"){let{listIosDevices:e}=await Promise.resolve().then(()=>({listIosDevices:G}));t.push(...await e())}else{let{listAndroidDevices:e}=await Promise.resolve().then(()=>({listAndroidDevices:y})),{listIosDevices:i}=await Promise.resolve().then(()=>({listIosDevices:G}));try{t.push(...await e())}catch{}try{t.push(...await i())}catch{}}return{ok:!0,data:{devices:t}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message,details:e.details}}}if("apps"===t){let t=e_.get(i),a=e.flags??{};if(!t&&!a.platform&&!a.device&&!a.udid&&!a.serial)return{ok:!1,error:{code:"INVALID_ARGS",message:"apps requires an active session or an explicit device selector (e.g. --platform ios)."}};let n=t?.device??await eA(a);if(await eJ(n),"ios"===n.platform){if("simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"apps list is only supported on iOS simulators"}};let{listSimulatorApps:e}=await Promise.resolve().then(()=>({listSimulatorApps:en}));return{ok:!0,data:{apps:(await e(n)).map(e=>e.name&&e.name!==e.bundleId?`${e.name} (${e.bundleId})`:e.bundleId)}}}let{listAndroidApps:o}=await Promise.resolve().then(()=>({listAndroidApps:S}));return{ok:!0,data:{apps:await o(n,e.flags?.appsFilter)}}}if("open"===t){let a;if(e_.has(i))return{ok:!1,error:{code:"INVALID_ARGS",message:"Session already active. Close it first or pass a new --session name."}};let n=await eA(e.flags??{});await eJ(n);let o=Array.from(e_.values()).find(e=>e.device.id===n.id);if(o)return{ok:!1,error:{code:"DEVICE_IN_USE",message:`Device is already in use by session "${o.name}".`,details:{session:o.name,deviceId:n.id,deviceName:n.name}}};let r=e.positionals?.[0];if("ios"===n.platform)try{let{resolveIosApp:t}=await Promise.resolve().then(()=>({resolveIosApp:q}));a=await t(n,e.positionals?.[0]??"")}catch{a=void 0}await eS(n,"open",e.positionals??[],e.flags?.out,{...eM(e.flags,a)});let s={name:i,device:n,createdAt:Date.now(),appBundleId:a,appName:r,actions:[]};return eU(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:i}}),e_.set(i,s),{ok:!0,data:{session:i}}}if("replay"===t){let t=e.positionals?.[0];if(!t)return{ok:!1,error:{code:"INVALID_ARGS",message:"replay requires a path"}};try{var a;let e=(a=t).startsWith("~/")?o.join(p.homedir(),a.slice(2)):o.resolve(a),n=JSON.parse(u.readFileSync(e,"utf8")),r=n.optimizedActions??n.actions??[];for(let e of r)e&&"replay"!==e.command&&await eF({token:eL,session:i,command:e.command,positionals:e.positionals??[],flags:e.flags??{}});return{ok:!0,data:{replayed:r.length,session:i}}}catch(t){let e=l(t);return{ok:!1,error:{code:e.code,message:e.message}}}}if("close"===t){let a=e_.get(i);return a?(e.positionals&&e.positionals.length>0&&await eS(a.device,"close",e.positionals??[],e.flags?.out,{...eM(e.flags,a.appBundleId)}),"ios"===a.device.platform&&"simulator"===a.device.kind&&await ec(a.device.id),eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{session:i}}),ej(a),e_.delete(i),{ok:!0,data:{session:i}}):{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session"}}}if("snapshot"===t){let a=e_.get(i),n=a?.device??await eA(e.flags??{});a||await eJ(n);let o=a?.appBundleId,r=e.flags?.snapshotScope;if(r&&r.trim().startsWith("@")){if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref scope requires an existing snapshot in session."}};let e=eO(r.trim());if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref scope: ${r}`}};let t=ek(a.snapshot.nodes,e),i=t?eB(t,a.snapshot.nodes):void 0;if(!i)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${r} not found or has no label`}};r=i}let s=await eS(n,"snapshot",[],e.flags?.out,{...eM({...e.flags,snapshotScope:r},o)}),l=s?.nodes??[],c=eD(e.flags?.snapshotRaw?l:function(e){let t=[],i=[];for(let a of e){let e=a.depth??0;for(;t.length>0&&e<=t[t.length-1];)t.pop();let n=function(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase();return t.startsWith("ax")&&(t=t.replace(/^ax/,"")),t}(a.type??""),o=[a.label,a.value,a.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0),r=!!o&&eG(o);if(("group"===n||"ioscontentgroup"===n)&&!r){t.push(e);continue}let s=Math.max(0,e-t.length);i.push({...a,depth:s})}return i}(l)),d={nodes:c,truncated:s?.truncated,createdAt:Date.now(),backend:s?.backend},u={name:i,device:n,createdAt:a?.createdAt??Date.now(),appBundleId:o,snapshot:d,actions:a?.actions??[]};return eU(u,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{nodes:c.length,truncated:s?.truncated??!1}}),e_.set(i,u),{ok:!0,data:{nodes:c,truncated:s?.truncated??!1,appName:a?.appName??o??n.name,appBundleId:o}}}if("wait"===t){let a=e_.get(i),n=a?.device??await eA(e.flags??{});a||await eJ(n);let o=e.positionals??[];if(0===o.length)return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires a duration or text"}};let r=e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null},s=r(o[0]);if(null!==s)return await new Promise(e=>setTimeout(e,s)),a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{waitedMs:s}}),{ok:!0,data:{waitedMs:s}};let l="",c=null;if("text"===o[0])l=null!==(c=r(o[o.length-1]))?o.slice(1,-1).join(" "):o.slice(1).join(" ");else if(o[0].startsWith("@")){if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"Ref wait requires an existing snapshot in session."}};let e=eO(o[0]);if(!e)return{ok:!1,error:{code:"INVALID_ARGS",message:`Invalid ref: ${o[0]}`}};let t=ek(a.snapshot.nodes,e),i=t?eB(t,a.snapshot.nodes):void 0;if(!i)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${o[0]} not found or has no label`}};c=r(o[o.length-1]),l=i}else l=null!==(c=r(o[o.length-1]))?o.slice(0,-1).join(" "):o.join(" ");if(!(l=l.trim()))return{ok:!1,error:{code:"INVALID_ARGS",message:"wait requires text"}};let d=c??1e4,u=Date.now();for(;Date.now()-u<d;){if("ios"===n.platform&&"simulator"===n.kind){let i=await el(n,{command:"findText",text:l,appBundleId:a?.appBundleId},{verbose:e.flags?.verbose,logPath:eR});if(i?.found)return a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-u}}),{ok:!0,data:{text:l,waitedMs:Date.now()-u}}}else if("android"!==n.platform)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"wait is not supported on this device"}};else if(eV(eD((await j(n,{scope:l})).nodes??[]),l))return a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{text:l,waitedMs:Date.now()-u}}),{ok:!0,data:{text:l,waitedMs:Date.now()-u}};await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:`wait timed out for text: ${l}`}}}if("alert"===t){let a=e_.get(i),n=a?.device??await eA(e.flags??{});a||await eJ(n);let o=(e.positionals?.[0]??"get").toLowerCase();if("ios"!==n.platform||"simulator"!==n.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"alert is only supported on iOS simulators in v1"}};if("wait"===o){let i=(e=>{if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null})(e.positionals?.[1])??1e4,o=Date.now();for(;Date.now()-o<i;){try{let i=await el(n,{command:"alert",action:"get",appBundleId:a?.appBundleId},{verbose:e.flags?.verbose,logPath:eR});return a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:i}),{ok:!0,data:i}}catch{}await new Promise(e=>setTimeout(e,300))}return{ok:!1,error:{code:"COMMAND_FAILED",message:"alert wait timed out"}}}let r=await el(n,{command:"alert",action:"accept"===o||"dismiss"===o?o:"get",appBundleId:a?.appBundleId},{verbose:e.flags?.verbose,logPath:eR});return a&&eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:r}),{ok:!0,data:r}}if("record"===t){let a=(e.positionals?.[0]??"").toLowerCase();if(!["start","stop"].includes(a))return{ok:!1,error:{code:"INVALID_ARGS",message:"record requires start|stop"}};let n=e_.get(i),r=n?.device??await eA(e.flags??{});n||await eJ(r);let s=n??{name:i,device:r,createdAt:Date.now(),actions:[]};if("start"===a){if(s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"recording already in progress"}};let a=e.positionals?.[1]??`./recording-${Date.now()}.mp4`,n=o.resolve(a),l=o.dirname(n);if(u.existsSync(l)||u.mkdirSync(l,{recursive:!0}),"ios"===r.platform){if("simulator"!==r.kind)return{ok:!1,error:{code:"UNSUPPORTED_OPERATION",message:"record is only supported on iOS simulators in v1"}};let{child:e,wait:t}=d("xcrun",["simctl","io",r.id,"recordVideo",n],{allowFailure:!0});s.recording={platform:"ios",outPath:n,child:e,wait:t}}else{let e=`/sdcard/agent-device-recording-${Date.now()}.mp4`,{child:t,wait:i}=d("adb",["-s",r.id,"shell","screenrecord",e],{allowFailure:!0});s.recording={platform:"android",outPath:n,remotePath:e,child:t,wait:i}}return e_.set(i,s),eU(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"start"}}),{ok:!0,data:{recording:"started",outPath:a}}}if(!s.recording)return{ok:!1,error:{code:"INVALID_ARGS",message:"no active recording"}};let l=s.recording;l.child.kill("SIGINT");try{await l.wait}catch{}if("android"===l.platform&&l.remotePath)try{await m("adb",["-s",r.id,"pull",l.remotePath,l.outPath],{allowFailure:!0}),await m("adb",["-s",r.id,"shell","rm","-f",l.remotePath],{allowFailure:!0})}catch{}return s.recording=void 0,eU(s,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{action:"stop",outPath:l.outPath}}),{ok:!0,data:{recording:"stopped",outPath:l.outPath}}}if("click"===t){let a=e_.get(i);if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let n=e.positionals?.[0]??"",o=eO(n);if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"click requires a ref like @e2"}};let r=ek(a.snapshot.nodes,o);if(!r?.rect&&e.positionals.length>1){let t=e.positionals.slice(1).join(" ").trim();t.length>0&&(r=eV(a.snapshot.nodes,t))}if(!r?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found or has no bounds`}};let s=eB(r,a.snapshot.nodes),l=r.label?.trim();if("ios"===a.device.platform&&"simulator"===a.device.kind&&l&&function(e,t){let i=t.trim().toLowerCase();if(!i)return!1;let a=0;for(let t of e)if((t.label??"").trim().toLowerCase()===i&&(a+=1)>1)return!1;return 1===a}(a.snapshot.nodes,l))return await el(a.device,{command:"tap",text:l,appBundleId:a.appBundleId},{verbose:e.flags?.verbose,logPath:eR}),eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,refLabel:l,mode:"text"}}),{ok:!0,data:{ref:o,mode:"text"}};let{x:c,y:d}=ex(r.rect);return await eS(a.device,"press",[String(c),String(d)],e.flags?.out,{...eM(e.flags,a.appBundleId)}),eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:o,x:c,y:d,refLabel:s}}),{ok:!0,data:{ref:o,x:c,y:d}}}if("fill"===t){let a=e_.get(i);if(e.positionals?.[0]?.startsWith("@")){if(!a?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let i=eO(e.positionals[0]);if(!i)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires a ref like @e2"}};let n=e.positionals.length>=3?e.positionals[1]:"",o=e.positionals.length>=3?e.positionals.slice(2).join(" "):e.positionals.slice(1).join(" ");if(!o)return{ok:!1,error:{code:"INVALID_ARGS",message:"fill requires text after ref"}};let r=ek(a.snapshot.nodes,i);if(!r?.rect&&n&&(r=eV(a.snapshot.nodes,n)),!r?.rect)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${e.positionals[0]} not found or has no bounds`}};let s=eB(r,a.snapshot.nodes),{x:l,y:c}=ex(r.rect),d=await eS(a.device,"fill",[String(l),String(c),o],e.flags?.out,{...eM(e.flags,a.appBundleId)});return eU(a,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:d??{ref:i,x:l,y:c,refLabel:s}}),{ok:!0,data:d??{ref:i,x:l,y:c}}}}if("get"===t){let a=e.positionals?.[0],n=e.positionals?.[1];if("text"!==a&&"attrs"!==a)return{ok:!1,error:{code:"INVALID_ARGS",message:"get only supports text or attrs"}};let o=e_.get(i);if(!o?.snapshot)return{ok:!1,error:{code:"INVALID_ARGS",message:"No snapshot in session. Run snapshot first."}};let r=eO(n??"");if(!r)return{ok:!1,error:{code:"INVALID_ARGS",message:"get text requires a ref like @e2"}};let s=ek(o.snapshot.nodes,r);if(!s&&e.positionals.length>2){let t=e.positionals.slice(2).join(" ").trim();t.length>0&&(s=eV(o.snapshot.nodes,t))}if(!s)return{ok:!1,error:{code:"COMMAND_FAILED",message:`Ref ${n} not found`}};if("attrs"===a)return eU(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r}}),{ok:!0,data:{ref:r,node:s}};let l=[s.label,s.value,s.identifier].map(e=>"string"==typeof e?e.trim():"").filter(e=>e.length>0)[0]??"";return eU(o,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:{ref:r,text:l,refLabel:l||void 0}}),{ok:!0,data:{ref:r,text:l,node:s}}}let n=e_.get(i);if(!n)return{ok:!1,error:{code:"SESSION_NOT_FOUND",message:"No active session. Run open first."}};let r=await eS(n.device,t,e.positionals??[],e.flags?.out,{...eM(e.flags,n.appBundleId)});return eU(n,{command:t,positionals:e.positionals??[],flags:e.flags??{},result:r??{}}),{ok:!0,data:r??{}}}function eU(e,t){t.flags?.noRecord||e.actions.push({ts:Date.now(),command:t.command,positionals:t.positionals,flags:function(e){if(!e)return{};let{platform:t,device:i,udid:a,serial:n,out:o,verbose:r,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:d,snapshotRaw:u,snapshotBackend:p,noRecord:f,recordJson:m}=e;return{platform:t,device:i,udid:a,serial:n,out:o,verbose:r,snapshotInteractiveOnly:s,snapshotCompact:l,snapshotDepth:c,snapshotScope:d,snapshotRaw:u,snapshotBackend:p,noRecord:f,recordJson:m}}(t.flags),result:t.result})}function ej(e){try{u.existsSync(eC)||u.mkdirSync(eC,{recursive:!0});let t=e.name.replace(/[^a-zA-Z0-9._-]/g,"_"),i=new Date(e.createdAt).toISOString().replace(/[:.]/g,"-"),a=o.join(eC,`${t}-${i}.ad`),n=o.join(eC,`${t}-${i}.json`),r={name:e.name,device:e.device,createdAt:e.createdAt,appBundleId:e.appBundleId,actions:e.actions,optimizedActions:function(e){let t=[];for(let i of e.actions)if("snapshot"!==i.command){if("click"===i.command||"fill"===i.command||"get"===i.command){let a=i.result?.refLabel;"string"==typeof a&&a.trim().length>0&&t.push({ts:i.ts,command:"snapshot",positionals:[],flags:{platform:e.device.platform,snapshotInteractiveOnly:!0,snapshotCompact:!0,snapshotScope:a.trim()},result:{scope:a.trim()}})}t.push(i)}return t}(e)},s=function(e,t){let i=[],a=e.device.name.replace(/"/g,'\\"'),n=e.device.kind?` kind=${e.device.kind}`:"";for(let o of(i.push(`context platform=${e.device.platform} device="${a}"${n} theme=unknown`),t))o.flags?.noRecord||i.push(function(e){let t=[e.command];if("click"===e.command){let i=e.positionals?.[0];if(i){t.push(e$(i));let a=e.result?.refLabel;return"string"==typeof a&&a.trim().length>0&&t.push(e$(a)),t.join(" ")}}if("fill"===e.command){let i=e.positionals?.[0];if(i&&i.startsWith("@")){t.push(e$(i));let a=e.result?.refLabel,n=e.positionals.slice(1).join(" ");return"string"==typeof a&&a.trim().length>0&&t.push(e$(a)),n&&t.push(e$(n)),t.join(" ")}}if("get"===e.command){let i=e.positionals?.[0],a=e.positionals?.[1];if(i&&a){t.push(e$(i)),t.push(e$(a));let n=e.result?.refLabel;return"string"==typeof n&&n.trim().length>0&&t.push(e$(n)),t.join(" ")}}if("snapshot"===e.command)return e.flags?.snapshotInteractiveOnly&&t.push("-i"),e.flags?.snapshotCompact&&t.push("-c"),"number"==typeof e.flags?.snapshotDepth&&t.push("-d",String(e.flags.snapshotDepth)),e.flags?.snapshotScope&&t.push("-s",e$(e.flags.snapshotScope)),e.flags?.snapshotRaw&&t.push("--raw"),e.flags?.snapshotBackend&&t.push("--backend",e.flags.snapshotBackend),t.join(" ");for(let i of e.positionals??[])t.push(e$(i));return t.join(" ")}(o));return`${i.join("\n")} | ||
| `}(e,r.optimizedActions);u.writeFileSync(a,s),e.actions.some(e=>e.flags?.recordJson)&&u.writeFileSync(n,JSON.stringify(r,null,2))}catch{}}function e$(e){let t=e.trim();return t.startsWith("@")||/^-?\d+(\.\d+)?$/.test(t)?t:JSON.stringify(t)}function eV(e,t){let i=t.toLowerCase();return e.find(e=>{let t=(e.label??"").toLowerCase(),a=(e.value??"").toLowerCase(),n=(e.identifier??"").toLowerCase();return t.includes(i)||a.includes(i)||n.includes(i)})??null}function eB(e,t){let i=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);return i&&eG(i)?i:function(e,t){if(!e.rect)return;let i=e.rect.y+e.rect.height/2,a=null;for(let e of t){if(!e.rect)continue;let t=[e.label,e.value,e.identifier].map(e=>"string"==typeof e?e.trim():"").find(e=>e&&e.length>0);if(!t||!eG(t))continue;let n=Math.abs(e.rect.y+e.rect.height/2-i);(!a||n<a.distance)&&(a={label:t,distance:n})}return a?.label}(e,t)??(i&&eG(i)?i:void 0)}function eG(e){let t=e.trim();return!(!t||/^(true|false)$/i.test(t)||/^\d+$/.test(t))}async function eJ(e){if("ios"===e.platform&&"simulator"===e.kind){let{ensureBootedSimulator:t}=await Promise.resolve().then(()=>({ensureBootedSimulator:eo}));await t(e);return}if("android"===e.platform){let{waitForAndroidBoot:t}=await Promise.resolve().then(()=>({waitForAndroidBoot:N}));await t(e.id)}}(e=h.createServer(e=>{let t="";e.setEncoding("utf8"),e.on("data",async i=>{let a=(t+=i).indexOf("\n");for(;-1!==a;){let i,n=t.slice(0,a).trim();if(t=t.slice(a+1),0===n.length){a=t.indexOf("\n");continue}try{let e=JSON.parse(n);i=await eF(e)}catch(t){let e=l(t);i={ok:!1,error:{code:e.code,message:e.message,details:e.details}}}e.write(`${JSON.stringify(i)} | ||
| `),a=t.indexOf("\n")}})})).listen(0,"127.0.0.1",()=>{let t=e.address();if("object"==typeof t&&t?.port){var i;i=t.port,u.existsSync(eE)||u.mkdirSync(eE,{recursive:!0}),u.writeFileSync(eR,""),u.writeFileSync(eP,JSON.stringify({port:i,token:eL,pid:process.pid,version:eT},null,2),{mode:384}),process.stdout.write(`AGENT_DEVICE_DAEMON_PORT=${t.port} | ||
| `)}}),t=async()=>{for(let e of Array.from(e_.values()))"ios"===e.device.platform&&"simulator"===e.device.kind&&await ec(e.device.id),ej(e);e.close(()=>{u.existsSync(eP)&&u.unlinkSync(eP),process.exit(0)})},process.on("SIGINT",()=>{t()}),process.on("SIGTERM",()=>{t()}),process.on("SIGHUP",()=>{t()}),process.on("uncaughtException",e=>{let i=e instanceof f?e:l(e);process.stderr.write(`Daemon error: ${i.message} | ||
| `),t()}); |
@@ -252,17 +252,2 @@ // | ||
| return Response(ok: true, data: DataPayload(found: found)) | ||
| case .rect: | ||
| guard let text = command.text else { | ||
| return Response(ok: false, error: ErrorPayload(message: "rect requires text")) | ||
| } | ||
| guard let element = findElement(app: activeApp, text: text) else { | ||
| return Response(ok: false, error: ErrorPayload(message: "element not found")) | ||
| } | ||
| let frame = element.frame | ||
| let rect = SnapshotRect( | ||
| x: Double(frame.origin.x), | ||
| y: Double(frame.origin.y), | ||
| width: Double(frame.size.width), | ||
| height: Double(frame.size.height) | ||
| ) | ||
| return Response(ok: true, data: DataPayload(rect: rect)) | ||
| case .listTappables: | ||
@@ -291,5 +276,58 @@ let elements = activeApp.descendants(matching: .any).allElementsBoundByIndex | ||
| return Response(ok: true, data: snapshotFast(app: activeApp, options: options)) | ||
| case .back: | ||
| if tapNavigationBack(app: activeApp) { | ||
| return Response(ok: true, data: DataPayload(message: "back")) | ||
| } | ||
| performBackGesture(app: activeApp) | ||
| return Response(ok: true, data: DataPayload(message: "back")) | ||
| case .home: | ||
| XCUIDevice.shared.press(.home) | ||
| return Response(ok: true, data: DataPayload(message: "home")) | ||
| case .appSwitcher: | ||
| performAppSwitcherGesture(app: activeApp) | ||
| return Response(ok: true, data: DataPayload(message: "appSwitcher")) | ||
| case .alert: | ||
| let action = (command.action ?? "get").lowercased() | ||
| let alert = activeApp.alerts.firstMatch | ||
| if !alert.exists { | ||
| return Response(ok: false, error: ErrorPayload(message: "alert not found")) | ||
| } | ||
| if action == "accept" { | ||
| let button = alert.buttons.allElementsBoundByIndex.first | ||
| button?.tap() | ||
| return Response(ok: true, data: DataPayload(message: "accepted")) | ||
| } | ||
| if action == "dismiss" { | ||
| let button = alert.buttons.allElementsBoundByIndex.last | ||
| button?.tap() | ||
| return Response(ok: true, data: DataPayload(message: "dismissed")) | ||
| } | ||
| let buttonLabels = alert.buttons.allElementsBoundByIndex.map { $0.label } | ||
| return Response(ok: true, data: DataPayload(message: alert.label, items: buttonLabels)) | ||
| } | ||
| } | ||
| private func tapNavigationBack(app: XCUIApplication) -> Bool { | ||
| let buttons = app.navigationBars.buttons.allElementsBoundByIndex | ||
| if let back = buttons.first(where: { $0.isHittable }) { | ||
| back.tap() | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
| private func performBackGesture(app: XCUIApplication) { | ||
| let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app | ||
| let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.05, dy: 0.5)) | ||
| let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5)) | ||
| start.press(forDuration: 0.05, thenDragTo: end) | ||
| } | ||
| private func performAppSwitcherGesture(app: XCUIApplication) { | ||
| let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app | ||
| let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99)) | ||
| let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.7)) | ||
| start.press(forDuration: 0.6, thenDragTo: end) | ||
| } | ||
| private func findElement(app: XCUIApplication, text: String) -> XCUIElement? { | ||
@@ -607,3 +645,6 @@ let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR identifier CONTAINS[c] %@ OR value CONTAINS[c] %@", text, text, text) | ||
| case snapshot | ||
| case rect | ||
| case back | ||
| case home | ||
| case appSwitcher | ||
| case alert | ||
| case shutdown | ||
@@ -623,2 +664,3 @@ } | ||
| let text: String? | ||
| let action: String? | ||
| let x: Double? | ||
@@ -652,3 +694,2 @@ let y: Double? | ||
| let truncated: Bool? | ||
| let rect: SnapshotRect? | ||
@@ -660,4 +701,3 @@ init( | ||
| nodes: [SnapshotNode]? = nil, | ||
| truncated: Bool? = nil, | ||
| rect: SnapshotRect? = nil | ||
| truncated: Bool? = nil | ||
| ) { | ||
@@ -669,3 +709,2 @@ self.message = message | ||
| self.truncated = truncated | ||
| self.rect = rect | ||
| } | ||
@@ -672,0 +711,0 @@ } |
@@ -22,7 +22,2 @@ import Foundation | ||
| struct AXSnapshot: Codable { | ||
| let windowFrame: AXNode.Frame? | ||
| let root: AXNode | ||
| } | ||
| struct AXSnapshotError: Error, CustomStringConvertible { | ||
@@ -34,2 +29,3 @@ let message: String | ||
| let simulatorBundleId = "com.apple.iphonesimulator" | ||
| let defaultMaxDepth = 40 | ||
@@ -56,13 +52,17 @@ func hasAccessibilityPermission() -> Bool { | ||
| func getChildren(_ element: AXUIElement) -> [AXUIElement] { | ||
| getAttribute(element, kAXChildrenAttribute as CFString) ?? [] | ||
| if let children: [AXUIElement] = getAttribute(element, kAXChildrenAttribute as CFString), | ||
| !children.isEmpty { | ||
| return children | ||
| } | ||
| if let children: [AXUIElement] = getAttribute(element, kAXVisibleChildrenAttribute as CFString), | ||
| !children.isEmpty { | ||
| return children | ||
| } | ||
| if let children: [AXUIElement] = getAttribute(element, kAXContentsAttribute as CFString), | ||
| !children.isEmpty { | ||
| return children | ||
| } | ||
| return [] | ||
| } | ||
| func getRole(_ element: AXUIElement) -> String? { | ||
| getAttribute(element, kAXRoleAttribute as CFString) | ||
| } | ||
| func getSubrole(_ element: AXUIElement) -> String? { | ||
| getAttribute(element, kAXSubroleAttribute as CFString) | ||
| } | ||
| func getLabel(_ element: AXUIElement) -> String? { | ||
@@ -78,2 +78,6 @@ if let label: String = getAttribute(element, "AXLabel" as CFString) { | ||
| func getDescription(_ element: AXUIElement) -> String? { | ||
| getAttribute(element, kAXDescriptionAttribute as CFString) | ||
| } | ||
| func getValue(_ element: AXUIElement) -> String? { | ||
@@ -118,7 +122,9 @@ if let value: String = getAttribute(element, kAXValueAttribute as CFString) { | ||
| func buildTree(_ element: AXUIElement, depth: Int = 0, maxDepth: Int = 40) -> AXNode { | ||
| let children = depth < maxDepth ? getChildren(element).map { buildTree($0, depth: depth + 1, maxDepth: maxDepth) } : [] | ||
| func buildTree(_ element: AXUIElement, depth: Int = 0, maxDepth: Int = defaultMaxDepth) -> AXNode { | ||
| let children = depth < maxDepth | ||
| ? getChildren(element).map { buildTree($0, depth: depth + 1, maxDepth: maxDepth) } | ||
| : [] | ||
| return AXNode( | ||
| role: getRole(element), | ||
| subrole: getSubrole(element), | ||
| role: getAttribute(element, kAXRoleAttribute as CFString), | ||
| subrole: getAttribute(element, kAXSubroleAttribute as CFString), | ||
| label: getLabel(element), | ||
@@ -132,15 +138,248 @@ value: getValue(element), | ||
| func findIOSAppRoot(in simulator: NSRunningApplication) -> (AXUIElement, AXNode.Frame?)? { | ||
| func findIOSAppSnapshot(in simulator: NSRunningApplication) -> (AXUIElement, AXNode.Frame?, AXUIElement, [AXUIElement], [AXUIElement])? { | ||
| let appElement = axElement(for: simulator) | ||
| let windows = getChildren(appElement).filter { getRole($0) == "AXWindow" } | ||
| let windows = getChildren(appElement).filter { | ||
| (getAttribute($0, kAXRoleAttribute as CFString) as String?) == (kAXWindowRole as String) | ||
| } | ||
| if windows.isEmpty { return nil } | ||
| if let focused: AXUIElement = getAttribute(appElement, kAXFocusedWindowAttribute as CFString), | ||
| let root = chooseRoot(in: focused) { | ||
| let extras = dedupeElements(findToolbarExtras(in: focused, root: root) + findTabBarExtras(in: focused, root: root)) | ||
| let modalRoots = findAdditionalWindowRoots(windows: windows, excluding: focused, windowFrame: getFrame(focused)) | ||
| return (root, getFrame(focused), focused, extras, modalRoots) | ||
| } | ||
| let sorted = windows.sorted { lhs, rhs in | ||
| let l = getFrame(lhs) | ||
| let r = getFrame(rhs) | ||
| let la = (l?.width ?? 0) * (l?.height ?? 0) | ||
| let ra = (r?.width ?? 0) * (r?.height ?? 0) | ||
| return la > ra | ||
| } | ||
| for window in sorted { | ||
| if let root = chooseRoot(in: window) { | ||
| let extras = dedupeElements(findToolbarExtras(in: window, root: root) + findTabBarExtras(in: window, root: root)) | ||
| let modalRoots = findAdditionalWindowRoots(windows: windows, excluding: window, windowFrame: getFrame(window)) | ||
| return (root, getFrame(window), window, extras, modalRoots) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| private func findAdditionalWindowRoots( | ||
| windows: [AXUIElement], | ||
| excluding mainWindow: AXUIElement, | ||
| windowFrame: AXNode.Frame? | ||
| ) -> [AXUIElement] { | ||
| var roots: [AXUIElement] = [] | ||
| for window in windows { | ||
| for child in getChildren(window) { | ||
| if getRole(child) == "AXGroup" { | ||
| return (child, getFrame(window)) | ||
| if CFEqual(window, mainWindow) { continue } | ||
| let frame = getFrame(window) | ||
| if let windowFrame = windowFrame, !frameIntersects(frame, windowFrame) { | ||
| continue | ||
| } | ||
| if let root = chooseRoot(in: window) { | ||
| roots.append(root) | ||
| } | ||
| } | ||
| return dedupeElements(roots) | ||
| } | ||
| private func dedupeElements(_ elements: [AXUIElement]) -> [AXUIElement] { | ||
| var seen: Set<AXWrapper> = [] | ||
| var result: [AXUIElement] = [] | ||
| for element in elements { | ||
| let wrapper = AXWrapper(element) | ||
| if seen.contains(wrapper) { continue } | ||
| seen.insert(wrapper) | ||
| result.append(element) | ||
| } | ||
| return result | ||
| } | ||
| func chooseRoot(in window: AXUIElement) -> AXUIElement? { | ||
| let windowFrame = getFrame(window) | ||
| let candidates = findGroupCandidates(in: window, windowFrame: windowFrame) | ||
| if let best = candidates.first?.element { return best } | ||
| return findLargestChildCandidate(in: window, windowFrame: windowFrame) | ||
| } | ||
| private func findLargestChildCandidate(in window: AXUIElement, windowFrame: AXNode.Frame?) -> AXUIElement? { | ||
| var best: (element: AXUIElement, area: Double)? = nil | ||
| for child in getChildren(window) { | ||
| let children = getChildren(child) | ||
| if children.isEmpty { continue } | ||
| let area = frameArea(getFrame(child), windowFrame: windowFrame) | ||
| if area <= 0 { continue } | ||
| if best == nil || area > best!.area { | ||
| best = (child, area) | ||
| } | ||
| } | ||
| return best?.element | ||
| } | ||
| private func frameIntersects(_ frame: AXNode.Frame?, _ target: AXNode.Frame?) -> Bool { | ||
| guard let frame = frame, let target = target else { return false } | ||
| let fx1 = frame.x | ||
| let fy1 = frame.y | ||
| let fx2 = frame.x + frame.width | ||
| let fy2 = frame.y + frame.height | ||
| let tx1 = target.x | ||
| let ty1 = target.y | ||
| let tx2 = target.x + target.width | ||
| let ty2 = target.y + target.height | ||
| return fx1 < tx2 && fx2 > tx1 && fy1 < ty2 && fy2 > ty1 | ||
| } | ||
| private func isToolbarLike(_ element: AXUIElement) -> Bool { | ||
| let role = (getAttribute(element, kAXRoleAttribute as CFString) as String?) ?? "" | ||
| let subrole = (getAttribute(element, kAXSubroleAttribute as CFString) as String?) ?? "" | ||
| if role == (kAXToolbarRole as String) || | ||
| role == (kAXTabGroupRole as String) || | ||
| role == "AXTabBar" { | ||
| return true | ||
| } | ||
| if subrole == "AXTabBar" { | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
| private func isTabBarLike(_ element: AXUIElement) -> Bool { | ||
| let role = (getAttribute(element, kAXRoleAttribute as CFString) as String?) ?? "" | ||
| let subrole = (getAttribute(element, kAXSubroleAttribute as CFString) as String?) ?? "" | ||
| if role == (kAXTabGroupRole as String) || role == "AXTabBar" { return true } | ||
| if subrole == "AXTabBar" { return true } | ||
| let desc = (getDescription(element) ?? "").lowercased() | ||
| if desc.contains("tab bar") { return true } | ||
| let label = (getLabel(element) ?? "").lowercased() | ||
| if label.contains("tab bar") { return true } | ||
| return false | ||
| } | ||
| private func findToolbarExtras(in window: AXUIElement, root: AXUIElement) -> [AXUIElement] { | ||
| let rootFrame = getFrame(root) | ||
| let rootIds = collectDescendantWrappers(from: root) | ||
| var extras: [AXUIElement] = [] | ||
| var stack = getChildren(window) | ||
| while !stack.isEmpty { | ||
| let current = stack.removeLast() | ||
| if isToolbarLike(current) && !rootIds.contains(AXWrapper(current)) { | ||
| let frame = getFrame(current) | ||
| if frameIntersects(frame, rootFrame) { | ||
| extras.append(current) | ||
| } | ||
| } | ||
| stack.append(contentsOf: getChildren(current)) | ||
| } | ||
| return nil | ||
| return extras | ||
| } | ||
| private func findTabBarExtras(in window: AXUIElement, root: AXUIElement) -> [AXUIElement] { | ||
| let rootFrame = getFrame(root) | ||
| let rootIds = collectDescendantWrappers(from: root) | ||
| var extras: [AXUIElement] = [] | ||
| var stack = getChildren(window) | ||
| while !stack.isEmpty { | ||
| let current = stack.removeLast() | ||
| if isTabBarLike(current) && !rootIds.contains(AXWrapper(current)) { | ||
| let frame = getFrame(current) | ||
| if frameIntersects(frame, rootFrame) { | ||
| extras.append(current) | ||
| } | ||
| } | ||
| stack.append(contentsOf: getChildren(current)) | ||
| } | ||
| return extras | ||
| } | ||
| private struct GroupCandidate { | ||
| let element: AXUIElement | ||
| let area: Double | ||
| let childCount: Int | ||
| } | ||
| private func findGroupCandidates(in root: AXUIElement, windowFrame: AXNode.Frame?) -> [GroupCandidate] { | ||
| var candidates: [GroupCandidate] = [] | ||
| var visited: Set<AXWrapper> = [] | ||
| func walk(_ element: AXUIElement) { | ||
| let wrapper = AXWrapper(element) | ||
| if visited.contains(wrapper) { return } | ||
| visited.insert(wrapper) | ||
| let children = getChildren(element) | ||
| let role = (getAttribute(element, kAXRoleAttribute as CFString) as String?) ?? "" | ||
| let isContainer = role == (kAXGroupRole as String) || | ||
| role == (kAXScrollAreaRole as String) || | ||
| role == (kAXUnknownRole as String) | ||
| if isContainer { | ||
| let hasNonToolbarChild = children.contains { | ||
| ((getAttribute($0, kAXRoleAttribute as CFString) as String?) ?? "") != (kAXToolbarRole as String) | ||
| } | ||
| if hasNonToolbarChild { | ||
| let frame = getFrame(element) | ||
| let area = frameArea(frame, windowFrame: windowFrame) | ||
| if area > 0 { | ||
| let childCount = children.count | ||
| candidates.append( | ||
| GroupCandidate( | ||
| element: element, | ||
| area: area, | ||
| childCount: childCount | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| for child in children { | ||
| walk(child) | ||
| } | ||
| } | ||
| walk(root) | ||
| candidates.sort { lhs, rhs in | ||
| if lhs.area == rhs.area { return lhs.childCount > rhs.childCount } | ||
| return lhs.area > rhs.area | ||
| } | ||
| return candidates | ||
| } | ||
| private func frameArea(_ frame: AXNode.Frame?, windowFrame: AXNode.Frame?) -> Double { | ||
| guard let frame = frame else { return 0 } | ||
| if let windowFrame = windowFrame { | ||
| let windowArea = max(1.0, windowFrame.width * windowFrame.height) | ||
| let area = frame.width * frame.height | ||
| if area > windowArea { return 0 } | ||
| return area | ||
| } | ||
| return frame.width * frame.height | ||
| } | ||
| private final class AXWrapper: Hashable { | ||
| let element: AXUIElement | ||
| init(_ element: AXUIElement) { self.element = element } | ||
| func hash(into hasher: inout Hasher) { hasher.combine(CFHash(element)) } | ||
| static func == (lhs: AXWrapper, rhs: AXWrapper) -> Bool { | ||
| return CFEqual(lhs.element, rhs.element) | ||
| } | ||
| } | ||
| private func collectDescendantWrappers(from root: AXUIElement) -> Set<AXWrapper> { | ||
| var seen: Set<AXWrapper> = [] | ||
| var stack = [root] | ||
| while !stack.isEmpty { | ||
| let current = stack.removeLast() | ||
| let wrapper = AXWrapper(current) | ||
| if seen.contains(wrapper) { continue } | ||
| seen.insert(wrapper) | ||
| stack.append(contentsOf: getChildren(current)) | ||
| } | ||
| return seen | ||
| } | ||
| private struct SnapshotPayload: Codable { | ||
| let windowFrame: AXNode.Frame? | ||
| let root: AXNode | ||
| } | ||
| func main() throws { | ||
@@ -153,10 +392,48 @@ guard hasAccessibilityPermission() else { | ||
| } | ||
| guard let (root, windowFrame) = findIOSAppRoot(in: simulator) else { | ||
| let maxAttempts = 5 | ||
| var snapshot: (AXUIElement, AXNode.Frame?, AXUIElement, [AXUIElement], [AXUIElement])? = nil | ||
| for attempt in 0..<maxAttempts { | ||
| if let candidate = findIOSAppSnapshot(in: simulator) { | ||
| let (root, _, _, _, modalRoots) = candidate | ||
| if !getChildren(root).isEmpty || !modalRoots.isEmpty { | ||
| snapshot = candidate | ||
| break | ||
| } | ||
| } | ||
| if attempt < maxAttempts - 1 { | ||
| usleep(300_000) | ||
| } | ||
| } | ||
| guard let (root, windowFrame, _, extras, modalRoots) = snapshot else { | ||
| throw AXSnapshotError(message: "Could not find iOS app content in Simulator.") | ||
| } | ||
| let tree = buildTree(root) | ||
| let snapshot = AXSnapshot(windowFrame: windowFrame, root: tree) | ||
| var tree = buildTree(root) | ||
| if !extras.isEmpty { | ||
| let extraNodes = extras.map { buildTree($0) } | ||
| tree = AXNode( | ||
| role: tree.role, | ||
| subrole: tree.subrole, | ||
| label: tree.label, | ||
| value: tree.value, | ||
| identifier: tree.identifier, | ||
| frame: tree.frame, | ||
| children: tree.children + extraNodes | ||
| ) | ||
| } | ||
| if !modalRoots.isEmpty { | ||
| let modalNodes = modalRoots.map { buildTree($0) } | ||
| tree = AXNode( | ||
| role: tree.role, | ||
| subrole: tree.subrole, | ||
| label: tree.label, | ||
| value: tree.value, | ||
| identifier: tree.identifier, | ||
| frame: tree.frame, | ||
| children: tree.children + modalNodes | ||
| ) | ||
| } | ||
| let payload = SnapshotPayload(windowFrame: windowFrame, root: tree) | ||
| let encoder = JSONEncoder() | ||
| encoder.outputFormatting = [.sortedKeys] | ||
| let data = try encoder.encode(snapshot) | ||
| let data = try encoder.encode(payload) | ||
| if let json = String(data: data, encoding: .utf8) { | ||
@@ -163,0 +440,0 @@ print(json) |
+4
-3
| { | ||
| "name": "agent-device", | ||
| "version": "0.1.2", | ||
| "version": "0.1.3", | ||
| "description": "Unified control plane for physical and virtual devices via an agent-driven CLI.", | ||
@@ -19,4 +19,6 @@ "license": "MIT", | ||
| "build:swift": "swift build -c release --package-path ios-runner/AXSnapshot", | ||
| "build:axsnapshot": "pnpm build:swift && mkdir -p bin && cp -f ios-runner/AXSnapshot/.build/release/axsnapshot bin/axsnapshot && chmod +x bin/axsnapshot", | ||
| "build:axsnapshot": "pnpm build:swift && mkdir -p dist/bin && cp -f ios-runner/AXSnapshot/.build/release/axsnapshot dist/bin/axsnapshot && chmod +x dist/bin/axsnapshot", | ||
| "build:xcuitest": "AGENT_DEVICE_IOS_CLEAN_DERIVED=1 xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=iOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived", | ||
| "build:clis": "pnpm build:node && pnpm build:axsnapshot", | ||
| "build:all": "pnpm build:node && pnpm build:axsnapshot && pnpm build:xcuitest", | ||
| "format": "prettier --write .", | ||
@@ -33,3 +35,2 @@ "prepublishOnly": "pnpm build:node && pnpm build:axsnapshot", | ||
| "dist", | ||
| "bin/axsnapshot", | ||
| "ios-runner", | ||
@@ -36,0 +37,0 @@ "!ios-runner/**/.build", |
+16
-6
@@ -9,3 +9,3 @@ # agent-device | ||
| - Platforms: iOS (simulator + limited device support) and Android (emulator + device). | ||
| - Core commands: `open`, `press`, `long-press`, `focus`, `type`, `fill`, `scroll`, `scrollintoview`, `screenshot`, `close`. | ||
| - Core commands: `open`, `back`, `home`, `app-switcher`, `press`, `long-press`, `focus`, `type`, `fill`, `scroll`, `scrollintoview`, `wait`, `alert`, `screenshot`, `close`. | ||
| - Inspection commands: `snapshot` (accessibility tree). | ||
@@ -38,3 +38,7 @@ - Device tooling: `adb` (Android), `simctl`/`devicectl` (iOS via Xcode). | ||
| agent-device snapshot | ||
| agent-device snapshot -s @e7 | ||
| agent-device click @e7 | ||
| agent-device wait text "Camera" | ||
| agent-device alert wait 10000 | ||
| agent-device back | ||
| agent-device type "hello" | ||
@@ -48,5 +52,12 @@ agent-device screenshot --out ./screenshot.png | ||
| Coordinates: | ||
| - All coordinate-based commands (`press`, `long-press`, `focus`, `fill`) use device coordinates with origin at top-left. | ||
| - X increases to the right, Y increases downward. | ||
| iOS snapshots: | ||
| - Default backend is `ax` (fast). It requires enabling Accessibility for the terminal app in System Settings. | ||
| - If AX is not available, use `--backend xctest` explicitly. | ||
| - If AX shows a container like `group` or `tab bar` without children, re-snapshot with XCTest and scope to the container label: | ||
| `snapshot --backend xctest -s "<label>"` | ||
| - Use `-s @ref` to scope the snapshot to the label of a prior ref in the current session. | ||
@@ -63,6 +74,5 @@ Flags: | ||
| - `--backend ax|xctest` (snapshot only; defaults to `ax` on iOS) | ||
| - `open` without args boots/activates the target device/simulator without launching an app. | ||
| Sessions: | ||
| - `open` starts a session. | ||
| - `open` starts a session. Without args boots/activates the target device/simulator without launching an app. | ||
| - All interaction commands require an open session. | ||
@@ -73,3 +83,3 @@ - `close` stops the session and releases device resources. Pass an app to close it explicitly, or omit to just close the session. | ||
| Snapshot defaults to the AX backend on iOS simulators and falls back to XCTest if AX is unavailable. | ||
| Snapshot defaults to the AX backend on iOS simulators. Use `--backend xctest` if AX is unavailable. | ||
@@ -82,4 +92,4 @@ ## App resolution | ||
| ## iOS notes | ||
| - Input commands (`press`, `type`, `scroll`, etc.) are supported only on simulators in v1. | ||
| - Support depends on your Xcode version; the CLI reports `UNSUPPORTED_OPERATION` if `simctl io` lacks input operations. | ||
| - Input commands (`press`, `type`, `scroll`, etc.) are supported only on simulators in v1 and use the XCTest runner. | ||
| - `alert` and `scrollintoview` use the XCTest runner and are simulator-only in v1. | ||
@@ -86,0 +96,0 @@ ## Testing |
+22
-0
@@ -87,2 +87,24 @@ import { parseArgs, usage } from './utils/args.ts'; | ||
| } | ||
| if (response.data && typeof response.data === 'object') { | ||
| const data = response.data as Record<string, unknown>; | ||
| if (command === 'devices') { | ||
| const devices = Array.isArray((data as any).devices) ? (data as any).devices : []; | ||
| const lines = devices.map((d: any) => { | ||
| const name = d?.name ?? d?.id ?? 'unknown'; | ||
| const platform = d?.platform ?? 'unknown'; | ||
| const kind = d?.kind ? ` ${d.kind}` : ''; | ||
| const booted = typeof d?.booted === 'boolean' ? ` booted=${d.booted}` : ''; | ||
| return `${name} (${platform}${kind})${booted}`; | ||
| }); | ||
| process.stdout.write(`${lines.join('\n')}\n`); | ||
| if (logTailStopper) logTailStopper(); | ||
| return; | ||
| } | ||
| if (command === 'apps') { | ||
| const apps = Array.isArray((data as any).apps) ? (data as any).apps : []; | ||
| process.stdout.write(`${apps.join('\n')}\n`); | ||
| if (logTailStopper) logTailStopper(); | ||
| return; | ||
| } | ||
| } | ||
| if (logTailStopper) logTailStopper(); | ||
@@ -89,0 +111,0 @@ return; |
+66
-2
| import { AppError } from '../utils/errors.ts'; | ||
| import { selectDevice, type DeviceInfo } from '../utils/device.ts'; | ||
| import { listAndroidDevices } from '../platforms/android/devices.ts'; | ||
| import { ensureAdb, snapshotAndroid } from '../platforms/android/index.ts'; | ||
| import { appSwitcherAndroid, backAndroid, ensureAdb, homeAndroid, snapshotAndroid } from '../platforms/android/index.ts'; | ||
| import { listIosDevices } from '../platforms/ios/devices.ts'; | ||
@@ -26,2 +26,3 @@ import { getInteractor } from '../utils/interactors.ts'; | ||
| recordJson?: boolean; | ||
| appsFilter?: 'launchable' | 'user-installed' | 'all'; | ||
| }; | ||
@@ -193,4 +194,22 @@ | ||
| case 'scrollintoview': { | ||
| const text = positionals.join(' '); | ||
| const text = positionals.join(' ').trim(); | ||
| if (!text) throw new AppError('INVALID_ARGS', 'scrollintoview requires text'); | ||
| if (device.platform === 'ios' && device.kind === 'simulator') { | ||
| const maxAttempts = 8; | ||
| for (let attempt = 0; attempt < maxAttempts; attempt += 1) { | ||
| const found = (await runIosRunnerCommand( | ||
| device, | ||
| { command: 'findText', text, appBundleId: context?.appBundleId }, | ||
| { verbose: context?.verbose, logPath: context?.logPath }, | ||
| )) as { found?: boolean }; | ||
| if (found?.found) return { text, attempts: attempt + 1 }; | ||
| await runIosRunnerCommand( | ||
| device, | ||
| { command: 'swipe', direction: 'up', appBundleId: context?.appBundleId }, | ||
| { verbose: context?.verbose, logPath: context?.logPath }, | ||
| ); | ||
| await new Promise((resolve) => setTimeout(resolve, 300)); | ||
| } | ||
| throw new AppError('COMMAND_FAILED', `scrollintoview could not find text: ${text}`); | ||
| } | ||
| await interactor.scrollIntoView(text); | ||
@@ -204,2 +223,47 @@ return { text }; | ||
| } | ||
| case 'back': { | ||
| if (device.platform === 'ios') { | ||
| if (device.kind !== 'simulator') { | ||
| throw new AppError('UNSUPPORTED_OPERATION', 'back is only supported on iOS simulators in v1'); | ||
| } | ||
| await runIosRunnerCommand( | ||
| device, | ||
| { command: 'back', appBundleId: context?.appBundleId }, | ||
| { verbose: context?.verbose, logPath: context?.logPath }, | ||
| ); | ||
| return { action: 'back' }; | ||
| } | ||
| await backAndroid(device); | ||
| return { action: 'back' }; | ||
| } | ||
| case 'home': { | ||
| if (device.platform === 'ios') { | ||
| if (device.kind !== 'simulator') { | ||
| throw new AppError('UNSUPPORTED_OPERATION', 'home is only supported on iOS simulators in v1'); | ||
| } | ||
| await runIosRunnerCommand( | ||
| device, | ||
| { command: 'home', appBundleId: context?.appBundleId }, | ||
| { verbose: context?.verbose, logPath: context?.logPath }, | ||
| ); | ||
| return { action: 'home' }; | ||
| } | ||
| await homeAndroid(device); | ||
| return { action: 'home' }; | ||
| } | ||
| case 'app-switcher': { | ||
| if (device.platform === 'ios') { | ||
| if (device.kind !== 'simulator') { | ||
| throw new AppError('UNSUPPORTED_OPERATION', 'app-switcher is only supported on iOS simulators in v1'); | ||
| } | ||
| await runIosRunnerCommand( | ||
| device, | ||
| { command: 'appSwitcher', appBundleId: context?.appBundleId }, | ||
| { verbose: context?.verbose, logPath: context?.logPath }, | ||
| ); | ||
| return { action: 'app-switcher' }; | ||
| } | ||
| await appSwitcherAndroid(device); | ||
| return { action: 'app-switcher' }; | ||
| } | ||
| case 'snapshot': { | ||
@@ -206,0 +270,0 @@ const backend = context?.snapshotBackend ?? 'ax'; |
+321
-37
@@ -19,2 +19,4 @@ import net from 'node:net'; | ||
| import { runIosRunnerCommand, stopIosRunnerSession } from './platforms/ios/runner-client.ts'; | ||
| import { runCmd, runCmdBackground } from './utils/exec.ts'; | ||
| import { snapshotAndroid } from './platforms/android/index.ts'; | ||
@@ -41,2 +43,9 @@ type DaemonRequest = { | ||
| actions: SessionAction[]; | ||
| recording?: { | ||
| platform: 'ios' | 'android'; | ||
| outPath: string; | ||
| remotePath?: string; | ||
| child: ReturnType<typeof import('node:child_process').spawn>; | ||
| wait: Promise<import('./utils/exec.ts').ExecResult>; | ||
| }; | ||
| }; | ||
@@ -117,2 +126,67 @@ | ||
| if (command === 'devices') { | ||
| try { | ||
| const devices: DeviceInfo[] = []; | ||
| if (req.flags?.platform === 'android') { | ||
| const { listAndroidDevices } = await import('./platforms/android/devices.ts'); | ||
| devices.push(...(await listAndroidDevices())); | ||
| } else if (req.flags?.platform === 'ios') { | ||
| const { listIosDevices } = await import('./platforms/ios/devices.ts'); | ||
| devices.push(...(await listIosDevices())); | ||
| } else { | ||
| const { listAndroidDevices } = await import('./platforms/android/devices.ts'); | ||
| const { listIosDevices } = await import('./platforms/ios/devices.ts'); | ||
| try { | ||
| devices.push(...(await listAndroidDevices())); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| try { | ||
| devices.push(...(await listIosDevices())); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| } | ||
| return { ok: true, data: { devices } }; | ||
| } catch (err) { | ||
| const appErr = asAppError(err); | ||
| return { ok: false, error: { code: appErr.code, message: appErr.message, details: appErr.details } }; | ||
| } | ||
| } | ||
| if (command === 'apps') { | ||
| const session = sessions.get(sessionName); | ||
| const flags = req.flags ?? {}; | ||
| if ( | ||
| !session && | ||
| !flags.platform && | ||
| !flags.device && | ||
| !flags.udid && | ||
| !flags.serial | ||
| ) { | ||
| return { | ||
| ok: false, | ||
| error: { | ||
| code: 'INVALID_ARGS', | ||
| message: 'apps requires an active session or an explicit device selector (e.g. --platform ios).', | ||
| }, | ||
| }; | ||
| } | ||
| const device = session?.device ?? (await resolveTargetDevice(flags)); | ||
| await ensureDeviceReady(device); | ||
| if (device.platform === 'ios') { | ||
| if (device.kind !== 'simulator') { | ||
| return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'apps list is only supported on iOS simulators' } }; | ||
| } | ||
| const { listSimulatorApps } = await import('./platforms/ios/index.ts'); | ||
| const apps = (await listSimulatorApps(device)).map((app) => | ||
| app.name && app.name !== app.bundleId ? `${app.name} (${app.bundleId})` : app.bundleId, | ||
| ); | ||
| return { ok: true, data: { apps } }; | ||
| } | ||
| const { listAndroidApps } = await import('./platforms/android/index.ts'); | ||
| const apps = await listAndroidApps(device, req.flags?.appsFilter); | ||
| return { ok: true, data: { apps } }; | ||
| } | ||
| if (command === 'open') { | ||
@@ -232,4 +306,20 @@ if (sessions.has(sessionName)) { | ||
| const appBundleId = session?.appBundleId; | ||
| let snapshotScope = req.flags?.snapshotScope; | ||
| if (snapshotScope && snapshotScope.trim().startsWith('@')) { | ||
| if (!session?.snapshot) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: 'Ref scope requires an existing snapshot in session.' } }; | ||
| } | ||
| const ref = normalizeRef(snapshotScope.trim()); | ||
| if (!ref) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: `Invalid ref scope: ${snapshotScope}` } }; | ||
| } | ||
| const node = findNodeByRef(session.snapshot.nodes, ref); | ||
| const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined; | ||
| if (!resolved) { | ||
| return { ok: false, error: { code: 'COMMAND_FAILED', message: `Ref ${snapshotScope} not found or has no label` } }; | ||
| } | ||
| snapshotScope = resolved; | ||
| } | ||
| const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, { | ||
| ...contextFromFlags(req.flags, appBundleId), | ||
| ...contextFromFlags({ ...req.flags, snapshotScope }, appBundleId), | ||
| })) as { | ||
@@ -240,4 +330,4 @@ nodes?: RawSnapshotNode[]; | ||
| }; | ||
| const pruned = pruneGroupNodes(data?.nodes ?? []); | ||
| const nodes = attachRefs(pruned); | ||
| const rawNodes = data?.nodes ?? []; | ||
| const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes)); | ||
| const snapshot: SnapshotState = { | ||
@@ -275,2 +365,225 @@ nodes, | ||
| if (command === 'wait') { | ||
| const session = sessions.get(sessionName); | ||
| const device = session?.device ?? (await resolveTargetDevice(req.flags ?? {})); | ||
| if (!session) { | ||
| await ensureDeviceReady(device); | ||
| } | ||
| const args = req.positionals ?? []; | ||
| if (args.length === 0) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: 'wait requires a duration or text' } }; | ||
| } | ||
| const parseTimeout = (value: string | undefined): number | null => { | ||
| if (!value) return null; | ||
| const parsed = Number(value); | ||
| return Number.isFinite(parsed) ? parsed : null; | ||
| }; | ||
| const sleepMs = parseTimeout(args[0]); | ||
| if (sleepMs !== null) { | ||
| await new Promise((resolve) => setTimeout(resolve, sleepMs)); | ||
| if (session) { | ||
| recordAction(session, { command, positionals: req.positionals ?? [], flags: req.flags ?? {}, result: { waitedMs: sleepMs } }); | ||
| } | ||
| return { ok: true, data: { waitedMs: sleepMs } }; | ||
| } | ||
| let text = ''; | ||
| let timeoutMs: number | null = null; | ||
| if (args[0] === 'text') { | ||
| timeoutMs = parseTimeout(args[args.length - 1]); | ||
| text = timeoutMs !== null ? args.slice(1, -1).join(' ') : args.slice(1).join(' '); | ||
| } else if (args[0].startsWith('@')) { | ||
| if (!session?.snapshot) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: 'Ref wait requires an existing snapshot in session.' } }; | ||
| } | ||
| const ref = normalizeRef(args[0]); | ||
| if (!ref) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: `Invalid ref: ${args[0]}` } }; | ||
| } | ||
| const node = findNodeByRef(session.snapshot.nodes, ref); | ||
| const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined; | ||
| if (!resolved) { | ||
| return { ok: false, error: { code: 'COMMAND_FAILED', message: `Ref ${args[0]} not found or has no label` } }; | ||
| } | ||
| timeoutMs = parseTimeout(args[args.length - 1]); | ||
| text = resolved; | ||
| } else { | ||
| timeoutMs = parseTimeout(args[args.length - 1]); | ||
| text = timeoutMs !== null ? args.slice(0, -1).join(' ') : args.join(' '); | ||
| } | ||
| text = text.trim(); | ||
| if (!text) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: 'wait requires text' } }; | ||
| } | ||
| const timeout = timeoutMs ?? 10000; | ||
| const start = Date.now(); | ||
| while (Date.now() - start < timeout) { | ||
| if (device.platform === 'ios' && device.kind === 'simulator') { | ||
| const result = (await runIosRunnerCommand( | ||
| device, | ||
| { command: 'findText', text, appBundleId: session?.appBundleId }, | ||
| { verbose: req.flags?.verbose, logPath }, | ||
| )) as { found?: boolean }; | ||
| if (result?.found) { | ||
| if (session) { | ||
| recordAction(session, { command, positionals: req.positionals ?? [], flags: req.flags ?? {}, result: { text, waitedMs: Date.now() - start } }); | ||
| } | ||
| return { ok: true, data: { text, waitedMs: Date.now() - start } }; | ||
| } | ||
| } else if (device.platform === 'android') { | ||
| const androidResult = await snapshotAndroid(device, { scope: text }); | ||
| if (findNodeByLabel(attachRefs(androidResult.nodes ?? []), text)) { | ||
| if (session) { | ||
| recordAction(session, { command, positionals: req.positionals ?? [], flags: req.flags ?? {}, result: { text, waitedMs: Date.now() - start } }); | ||
| } | ||
| return { ok: true, data: { text, waitedMs: Date.now() - start } }; | ||
| } | ||
| } else { | ||
| return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'wait is not supported on this device' } }; | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, 300)); | ||
| } | ||
| return { ok: false, error: { code: 'COMMAND_FAILED', message: `wait timed out for text: ${text}` } }; | ||
| } | ||
| if (command === 'alert') { | ||
| const session = sessions.get(sessionName); | ||
| const device = session?.device ?? (await resolveTargetDevice(req.flags ?? {})); | ||
| if (!session) { | ||
| await ensureDeviceReady(device); | ||
| } | ||
| const action = (req.positionals?.[0] ?? 'get').toLowerCase(); | ||
| const parseTimeout = (value: string | undefined): number | null => { | ||
| if (!value) return null; | ||
| const parsed = Number(value); | ||
| return Number.isFinite(parsed) ? parsed : null; | ||
| }; | ||
| if (device.platform !== 'ios' || device.kind !== 'simulator') { | ||
| return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'alert is only supported on iOS simulators in v1' } }; | ||
| } | ||
| if (action === 'wait') { | ||
| const timeout = parseTimeout(req.positionals?.[1]) ?? 10000; | ||
| const start = Date.now(); | ||
| while (Date.now() - start < timeout) { | ||
| try { | ||
| const data = await runIosRunnerCommand( | ||
| device, | ||
| { command: 'alert', action: 'get', appBundleId: session?.appBundleId }, | ||
| { verbose: req.flags?.verbose, logPath }, | ||
| ); | ||
| if (session) { | ||
| recordAction(session, { | ||
| command, | ||
| positionals: req.positionals ?? [], | ||
| flags: req.flags ?? {}, | ||
| result: data, | ||
| }); | ||
| } | ||
| return { ok: true, data }; | ||
| } catch { | ||
| // keep waiting | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, 300)); | ||
| } | ||
| return { ok: false, error: { code: 'COMMAND_FAILED', message: 'alert wait timed out' } }; | ||
| } | ||
| const data = await runIosRunnerCommand( | ||
| device, | ||
| { | ||
| command: 'alert', | ||
| action: action === 'accept' || action === 'dismiss' ? (action as 'accept' | 'dismiss') : 'get', | ||
| appBundleId: session?.appBundleId, | ||
| }, | ||
| { verbose: req.flags?.verbose, logPath }, | ||
| ); | ||
| if (session) { | ||
| recordAction(session, { | ||
| command, | ||
| positionals: req.positionals ?? [], | ||
| flags: req.flags ?? {}, | ||
| result: data, | ||
| }); | ||
| } | ||
| return { ok: true, data }; | ||
| } | ||
| if (command === 'record') { | ||
| const action = (req.positionals?.[0] ?? '').toLowerCase(); | ||
| if (!['start', 'stop'].includes(action)) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: 'record requires start|stop' } }; | ||
| } | ||
| const session = sessions.get(sessionName); | ||
| const device = session?.device ?? (await resolveTargetDevice(req.flags ?? {})); | ||
| if (!session) { | ||
| await ensureDeviceReady(device); | ||
| } | ||
| const activeSession = session ?? { | ||
| name: sessionName, | ||
| device, | ||
| createdAt: Date.now(), | ||
| actions: [], | ||
| }; | ||
| if (action === 'start') { | ||
| if (activeSession.recording) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: 'recording already in progress' } }; | ||
| } | ||
| const outPath = req.positionals?.[1] ?? `./recording-${Date.now()}.mp4`; | ||
| const resolvedOut = path.resolve(outPath); | ||
| const outDir = path.dirname(resolvedOut); | ||
| if (!fs.existsSync(outDir)) { | ||
| fs.mkdirSync(outDir, { recursive: true }); | ||
| } | ||
| if (device.platform === 'ios') { | ||
| if (device.kind !== 'simulator') { | ||
| return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'record is only supported on iOS simulators in v1' } }; | ||
| } | ||
| const { child, wait } = runCmdBackground('xcrun', ['simctl', 'io', device.id, 'recordVideo', resolvedOut], { | ||
| allowFailure: true, | ||
| }); | ||
| activeSession.recording = { platform: 'ios', outPath: resolvedOut, child, wait }; | ||
| } else { | ||
| const remotePath = `/sdcard/agent-device-recording-${Date.now()}.mp4`; | ||
| const { child, wait } = runCmdBackground('adb', ['-s', device.id, 'shell', 'screenrecord', remotePath], { | ||
| allowFailure: true, | ||
| }); | ||
| activeSession.recording = { platform: 'android', outPath: resolvedOut, remotePath, child, wait }; | ||
| } | ||
| sessions.set(sessionName, activeSession); | ||
| recordAction(activeSession, { | ||
| command, | ||
| positionals: req.positionals ?? [], | ||
| flags: req.flags ?? {}, | ||
| result: { action: 'start' }, | ||
| }); | ||
| return { ok: true, data: { recording: 'started', outPath } }; | ||
| } | ||
| if (!activeSession.recording) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: 'no active recording' } }; | ||
| } | ||
| const recording = activeSession.recording; | ||
| recording.child.kill('SIGINT'); | ||
| try { | ||
| await recording.wait; | ||
| } catch { | ||
| // ignore | ||
| } | ||
| if (recording.platform === 'android' && recording.remotePath) { | ||
| try { | ||
| await runCmd('adb', ['-s', device.id, 'pull', recording.remotePath, recording.outPath], { allowFailure: true }); | ||
| await runCmd('adb', ['-s', device.id, 'shell', 'rm', '-f', recording.remotePath], { allowFailure: true }); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| } | ||
| activeSession.recording = undefined; | ||
| recordAction(activeSession, { | ||
| command, | ||
| positionals: req.positionals ?? [], | ||
| flags: req.flags ?? {}, | ||
| result: { action: 'stop', outPath: recording.outPath }, | ||
| }); | ||
| return { ok: true, data: { recording: 'stopped', outPath: recording.outPath } }; | ||
| } | ||
| if (command === 'click') { | ||
@@ -419,35 +732,2 @@ const session = sessions.get(sessionName); | ||
| if (command === 'rect') { | ||
| const session = sessions.get(sessionName); | ||
| if (!session?.snapshot) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: 'No snapshot in session. Run snapshot first.' } }; | ||
| } | ||
| const target = req.positionals?.[0] ?? ''; | ||
| const ref = normalizeRef(target); | ||
| let label = ''; | ||
| if (ref) { | ||
| const node = findNodeByRef(session.snapshot.nodes, ref); | ||
| label = node?.label?.trim() ?? ''; | ||
| } else { | ||
| label = req.positionals.join(' ').trim(); | ||
| } | ||
| if (!label) { | ||
| return { ok: false, error: { code: 'INVALID_ARGS', message: 'rect requires a label or ref with label' } }; | ||
| } | ||
| if (session.device.platform !== 'ios' || session.device.kind !== 'simulator') { | ||
| return { ok: false, error: { code: 'UNSUPPORTED_OPERATION', message: 'rect is only supported on iOS simulators' } }; | ||
| } | ||
| const data = await runIosRunnerCommand( | ||
| session.device, | ||
| { command: 'rect', text: label, appBundleId: session.appBundleId }, | ||
| { verbose: req.flags?.verbose, logPath }, | ||
| ); | ||
| recordAction(session, { | ||
| command, | ||
| positionals: req.positionals ?? [], | ||
| flags: req.flags ?? {}, | ||
| result: { label, rect: data?.rect }, | ||
| }); | ||
| return { ok: true, data: { label, rect: data?.rect } }; | ||
| } | ||
@@ -846,3 +1126,7 @@ const session = sessions.get(sessionName); | ||
| const type = normalizeType(node.type ?? ''); | ||
| if (type === 'group' || type === 'ioscontentgroup') { | ||
| const labelCandidate = [node.label, node.value, node.identifier] | ||
| .map((value) => (typeof value === 'string' ? value.trim() : '')) | ||
| .find((value) => value && value.length > 0); | ||
| const hasMeaningfulLabel = labelCandidate ? isMeaningfulLabel(labelCandidate) : false; | ||
| if ((type === 'group' || type === 'ioscontentgroup') && !hasMeaningfulLabel) { | ||
| skippedDepths.push(depth); | ||
@@ -849,0 +1133,0 @@ continue; |
@@ -46,2 +46,49 @@ import { promises as fs } from 'node:fs'; | ||
| export async function listAndroidApps( | ||
| device: DeviceInfo, | ||
| filter: 'launchable' | 'user-installed' | 'all' = 'launchable', | ||
| ): Promise<string[]> { | ||
| if (filter === 'launchable') { | ||
| const result = await runCmd( | ||
| 'adb', | ||
| adbArgs(device, [ | ||
| 'shell', | ||
| 'cmd', | ||
| 'package', | ||
| 'query-activities', | ||
| '--brief', | ||
| '-a', | ||
| 'android.intent.action.MAIN', | ||
| '-c', | ||
| 'android.intent.category.LAUNCHER', | ||
| ]), | ||
| { allowFailure: true }, | ||
| ); | ||
| if (result.exitCode === 0 && result.stdout.trim().length > 0) { | ||
| const packages = new Set<string>(); | ||
| for (const line of result.stdout.split('\n')) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed) continue; | ||
| const firstToken = trimmed.split(/\s+/)[0]; | ||
| const pkg = firstToken.includes('/') ? firstToken.split('/')[0] : firstToken; | ||
| if (pkg) packages.add(pkg); | ||
| } | ||
| if (packages.size > 0) { | ||
| return Array.from(packages); | ||
| } | ||
| } | ||
| // fallback: list all if query-activities not available | ||
| } | ||
| const args = | ||
| filter === 'user-installed' | ||
| ? ['shell', 'pm', 'list', 'packages', '-3'] | ||
| : ['shell', 'pm', 'list', 'packages']; | ||
| const result = await runCmd('adb', adbArgs(device, args)); | ||
| return result.stdout | ||
| .split('\n') | ||
| .map((line: string) => line.replace('package:', '').trim()) | ||
| .filter(Boolean); | ||
| } | ||
| export async function openAndroidApp(device: DeviceInfo, app: string): Promise<void> { | ||
@@ -93,2 +140,14 @@ if (!device.booted) { | ||
| export async function backAndroid(device: DeviceInfo): Promise<void> { | ||
| await runCmd('adb', adbArgs(device, ['shell', 'input', 'keyevent', '4'])); | ||
| } | ||
| export async function homeAndroid(device: DeviceInfo): Promise<void> { | ||
| await runCmd('adb', adbArgs(device, ['shell', 'input', 'keyevent', '3'])); | ||
| } | ||
| export async function appSwitcherAndroid(device: DeviceInfo): Promise<void> { | ||
| await runCmd('adb', adbArgs(device, ['shell', 'input', 'keyevent', '187'])); | ||
| } | ||
| export async function longPressAndroid( | ||
@@ -95,0 +154,0 @@ device: DeviceInfo, |
@@ -35,2 +35,6 @@ import path from 'node:path'; | ||
| } | ||
| if (stderrText.toLowerCase().includes('could not find ios app content')) { | ||
| hint = | ||
| ' AX snapshot sometimes caches empty content. Try restarting the Simulator app.'; | ||
| } | ||
| throw new AppError('COMMAND_FAILED', 'AX snapshot failed', { | ||
@@ -131,10 +135,4 @@ stderr: `${stderrText}${hint}`, | ||
| if (envPath && fs.existsSync(envPath)) return envPath; | ||
| const packagedCandidates = [ | ||
| path.join(projectRoot, 'bin', 'axsnapshot'), | ||
| path.join(projectRoot, 'dist', 'bin', 'axsnapshot'), | ||
| path.join(projectRoot, 'dist', 'axsnapshot'), | ||
| ]; | ||
| for (const candidate of packagedCandidates) { | ||
| if (fs.existsSync(candidate)) return candidate; | ||
| } | ||
| const packagedPath = path.join(projectRoot, 'dist', 'bin', 'axsnapshot'); | ||
| if (fs.existsSync(packagedPath)) return packagedPath; | ||
| const binaryPath = path.join(packageDir, '.build', 'release', 'axsnapshot'); | ||
@@ -141,0 +139,0 @@ if (fs.existsSync(binaryPath)) return binaryPath; |
@@ -166,3 +166,3 @@ import { runCmd } from '../../utils/exec.ts'; | ||
| async function listSimulatorApps( | ||
| export async function listSimulatorApps( | ||
| device: DeviceInfo, | ||
@@ -172,15 +172,36 @@ ): Promise<{ bundleId: string; name: string }[]> { | ||
| const stdout = result.stdout as string; | ||
| if (!stdout.trim().startsWith('{')) return []; | ||
| try { | ||
| const payload = JSON.parse(stdout) as Record< | ||
| string, | ||
| { CFBundleDisplayName?: string; CFBundleName?: string } | ||
| >; | ||
| return Object.entries(payload).map(([bundleId, info]) => ({ | ||
| bundleId, | ||
| name: info.CFBundleDisplayName ?? info.CFBundleName ?? bundleId, | ||
| })); | ||
| } catch { | ||
| return []; | ||
| const trimmed = stdout.trim(); | ||
| if (!trimmed) return []; | ||
| let parsed: Record<string, { CFBundleDisplayName?: string; CFBundleName?: string }> | null = null; | ||
| if (trimmed.startsWith('{')) { | ||
| try { | ||
| parsed = JSON.parse(trimmed) as Record< | ||
| string, | ||
| { CFBundleDisplayName?: string; CFBundleName?: string } | ||
| >; | ||
| } catch { | ||
| parsed = null; | ||
| } | ||
| } | ||
| if (!parsed && trimmed.startsWith('{')) { | ||
| try { | ||
| const converted = await runCmd('plutil', ['-convert', 'json', '-o', '-', '-'], { | ||
| allowFailure: true, | ||
| stdin: trimmed, | ||
| }); | ||
| if (converted.exitCode === 0 && converted.stdout.trim().startsWith('{')) { | ||
| parsed = JSON.parse(converted.stdout) as Record< | ||
| string, | ||
| { CFBundleDisplayName?: string; CFBundleName?: string } | ||
| >; | ||
| } | ||
| } catch { | ||
| parsed = null; | ||
| } | ||
| } | ||
| if (!parsed) return []; | ||
| return Object.entries(parsed).map(([bundleId, info]) => ({ | ||
| bundleId, | ||
| name: info.CFBundleDisplayName ?? info.CFBundleName ?? bundleId, | ||
| })); | ||
| } | ||
@@ -187,0 +208,0 @@ |
@@ -11,5 +11,17 @@ import fs from 'node:fs'; | ||
| export type RunnerCommand = { | ||
| command: 'tap' | 'type' | 'swipe' | 'findText' | 'listTappables' | 'snapshot' | 'rect' | 'shutdown'; | ||
| command: | ||
| | 'tap' | ||
| | 'type' | ||
| | 'swipe' | ||
| | 'findText' | ||
| | 'listTappables' | ||
| | 'snapshot' | ||
| | 'back' | ||
| | 'home' | ||
| | 'appSwitcher' | ||
| | 'alert' | ||
| | 'shutdown'; | ||
| appBundleId?: string; | ||
| text?: string; | ||
| action?: 'get' | 'accept' | 'dismiss'; | ||
| x?: number; | ||
@@ -16,0 +28,0 @@ y?: number; |
+20
-1
@@ -21,2 +21,3 @@ import { AppError } from './errors.ts'; | ||
| snapshotBackend?: 'ax' | 'xctest'; | ||
| appsFilter?: 'launchable' | 'user-installed' | 'all'; | ||
| noRecord?: boolean; | ||
@@ -66,2 +67,10 @@ recordJson?: boolean; | ||
| } | ||
| if (arg === '--user-installed') { | ||
| flags.appsFilter = 'user-installed'; | ||
| continue; | ||
| } | ||
| if (arg === '--all') { | ||
| flags.appsFilter = 'all'; | ||
| continue; | ||
| } | ||
| if (arg.startsWith('--backend')) { | ||
@@ -161,4 +170,10 @@ const value = arg.includes('=') | ||
| xctest: XCTest snapshot (slower, no permissions) | ||
| devices List available devices | ||
| apps [--user-installed|--all] List installed apps (Android launchable by default, iOS simulator) | ||
| back Navigate back (where supported) | ||
| home Go to home screen (where supported) | ||
| app-switcher Open app switcher (where supported) | ||
| wait <ms>|text <text>|@ref [timeoutMs] Wait for duration or text to appear | ||
| alert [get|accept|dismiss|wait] [timeout] Inspect or handle alert (iOS simulator) | ||
| click <@ref> Click element by snapshot ref | ||
| rect <label|@ref> Fetch element frame by label or ref (iOS sim) | ||
| get text <@ref> Return element text by ref | ||
@@ -175,2 +190,4 @@ get attrs <@ref> Return element attributes by ref | ||
| screenshot [--out path] Capture screenshot | ||
| record start [path] Start screen recording | ||
| record stop Stop screen recording | ||
| session list List active sessions | ||
@@ -189,3 +206,5 @@ | ||
| --record-json Record JSON session log | ||
| --user-installed Apps: list user-installed packages (Android only) | ||
| --all Apps: list all packages (Android only) | ||
| `; | ||
| } |
+74
-3
@@ -16,2 +16,3 @@ import { spawn, spawnSync } from 'node:child_process'; | ||
| binaryStdout?: boolean; | ||
| stdin?: string | Buffer; | ||
| }; | ||
@@ -24,2 +25,7 @@ | ||
| export type ExecBackgroundResult = { | ||
| child: ReturnType<typeof spawn>; | ||
| wait: Promise<ExecResult>; | ||
| }; | ||
| export async function runCmd( | ||
@@ -34,3 +40,3 @@ cmd: string, | ||
| env: options.env, | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); | ||
@@ -45,2 +51,7 @@ | ||
| if (options.stdin !== undefined) { | ||
| child.stdin.write(options.stdin); | ||
| } | ||
| child.stdin.end(); | ||
| child.stdout.on('data', (chunk) => { | ||
@@ -103,4 +114,5 @@ if (options.binaryStdout) { | ||
| env: options.env, | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| encoding: options.binaryStdout ? undefined : 'utf8', | ||
| input: options.stdin, | ||
| }); | ||
@@ -162,3 +174,3 @@ | ||
| env: options.env, | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); | ||
@@ -173,2 +185,7 @@ | ||
| if (options.stdin !== undefined) { | ||
| child.stdin.write(options.stdin); | ||
| } | ||
| child.stdin.end(); | ||
| child.stdout.on('data', (chunk) => { | ||
@@ -221,2 +238,56 @@ if (options.binaryStdout) { | ||
| export function runCmdBackground( | ||
| cmd: string, | ||
| args: string[], | ||
| options: ExecOptions = {}, | ||
| ): ExecBackgroundResult { | ||
| const child = spawn(cmd, args, { | ||
| cwd: options.cwd, | ||
| env: options.env, | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| }); | ||
| let stdout = ''; | ||
| let stderr = ''; | ||
| child.stdout.setEncoding('utf8'); | ||
| child.stderr.setEncoding('utf8'); | ||
| child.stdout.on('data', (chunk) => { | ||
| stdout += chunk; | ||
| }); | ||
| child.stderr.on('data', (chunk) => { | ||
| stderr += chunk; | ||
| }); | ||
| const wait = new Promise<ExecResult>((resolve, reject) => { | ||
| child.on('error', (err) => { | ||
| const code = (err as NodeJS.ErrnoException).code; | ||
| if (code === 'ENOENT') { | ||
| reject(new AppError('TOOL_MISSING', `${cmd} not found in PATH`, { cmd }, err)); | ||
| return; | ||
| } | ||
| reject(new AppError('COMMAND_FAILED', `Failed to run ${cmd}`, { cmd, args }, err)); | ||
| }); | ||
| child.on('close', (code) => { | ||
| const exitCode = code ?? 1; | ||
| if (exitCode !== 0 && !options.allowFailure) { | ||
| reject( | ||
| new AppError('COMMAND_FAILED', `${cmd} exited with code ${exitCode}`, { | ||
| cmd, | ||
| args, | ||
| stdout, | ||
| stderr, | ||
| exitCode, | ||
| }), | ||
| ); | ||
| return; | ||
| } | ||
| resolve({ stdout, stderr, exitCode }); | ||
| }); | ||
| }); | ||
| return { child, wait }; | ||
| } | ||
| export function whichCmdSync(cmd: string): boolean { | ||
@@ -223,0 +294,0 @@ try { |
Sorry, the diff of this file is not supported yet
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 7 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 7 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
671201
17.33%4426
14.93%116
9.43%