@appetize/cli 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @appetize/cli
2
2
 
3
+ ## 0.25.0
4
+
5
+ ### Minor Changes
6
+
7
+ - feat(session): `session start` takes `--launch-app`, `--launch-url`, `--launch-params`, `--launch-permissions` and `--launch-ios-args`, with `--open` kept as an alias
8
+
9
+ ### Patch Changes
10
+
11
+ - fix(session): cancelling `session start` while connecting no longer crashes the daemon
12
+
13
+ ## 0.24.0
14
+
15
+ ### Minor Changes
16
+
17
+ - feat(session): `session start` sets the device's orientation, locale, timezone, location, appearance, font size, keyboards, autocorrect, soft keyboard and adb shell command
18
+
19
+ ### Patch Changes
20
+
21
+ - fix(session): `session start` errors include the webserver's reason
22
+ - fix(session): `session start` rejects invalid options before spawning its daemon
23
+
3
24
  ## 0.23.0
4
25
 
5
26
  ### Minor Changes
package/cli.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import qe from"sade";import{p as y,a as f,f as N,U as a,d as _,b as Ke,i as Ge,c as Ze,e as We,r as Q,g as T,h as ee,j as Xe,D as Ve,s as He,k as Je,l as te,m as oe,S as R,n as Ye,o as Qe,q as ne,t as et,u as tt,v as ot,w as nt,x as ie,y as se,z as it,A as st,B as rt,C as at,E as ct,F as pt}from"./stream-DqsoC5l1.js";import{existsSync as re,openAsBlob as dt,openSync as lt,closeSync as ut}from"node:fs";import{basename as U,extname as ft,resolve as ae,dirname as mt,join as x,relative as ht}from"node:path";import{writeFile as ce,stat as wt,rm as gt,cp as yt}from"node:fs/promises";import{spawn as vt}from"node:child_process";import{fileURLToPath as pe}from"node:url";import{humanId as xt}from"human-id";import{homedir as St}from"node:os";import bt from"debug";import Et from"yocto-spinner";import It from"cli-table3";import"json-rpc-2.0";import"stream";import"module";import"node:net";const de=["back","menu","unlock","volumeDown","volumeUp"],le=["home","lock"],ue=[...le,...de],j=["down","left","right","up"],fe=[{header:"Target",key:"id"},{header:"App",key:"appId"},{header:"Platform",key:"platform"},{header:"Version",key:"versionName"},{header:"Build",key:"buildNumber"},{header:"Uploaded",key:"created"}];function z({appId:e,buildNumber:t,created:o,id:n,platform:i,versionName:s}){return{appId:e,buildNumber:t,created:o,id:n,platform:i,versionName:s}}const Pt=["app"],At=10,Tt=[{fields:fe,key:"builds"},{header:"Total",key:"total"},{header:"Next page",key:"nextPage"}];async function Ot({options:e,signal:t}){const o=y(e.app,"--app"),n=f(e.limit,"--limit")??At,i=f(e.page,"--page"),s={limit:String(n)};o!==void 0&&(s.appId=o),i!==void 0&&(s.page=String(i));const r=await N("/api/v2/builds",{commandOptions:e,query:s,signal:t});return{builds:r.builds.map(z),nextPage:r.nextPage,total:r.total}}const kt=["note","tags"],me=12e4,$t=2e3,F=[".apk",".apks",".zip",".tar.gz",".tgz"],Lt={".apk":"android",".apks":"android",".tar.gz":"ios",".tgz":"ios",".zip":"ios"},Dt=fe;async function Nt({options:e,positionals:[t],progress:o,signal:n}){const i=Ut(t),s=U(t);o.status(`Uploading ${s}`);const r=await jt(t,i,e,n);if(e.wait!==!0)return o.status(`Uploaded ${s}`),z(r);o.step(`Uploaded ${s}`),o.status("Waiting for build metadata");const p=await _t(r.id,e,n);return o.status(`Processed ${s}`),z(p)}async function _t(e,t,o){const n=f(t.timeout,"--timeout")??me,i=Date.now()+n;for(;;){const s=await N(`/api/v2/builds/${e}`,{commandOptions:t,signal:o});if(s.appId!==void 0)return s;if(Date.now()>=i)throw new Error(`Build ${e} is still being processed after ${String(n)}ms; find it with 'appetize build list'`);await _($t,o)}}async function Rt(e){if(!re(e))throw new Error(`No build file at ${e}`);return await dt(e)}function Ut(e){const t=e.toLowerCase(),o=F.find(n=>t.endsWith(n));if(o===void 0)throw new a(`Cannot tell the platform of ${U(e)}; upload a ${F.join(", ")} file`);return Lt[o]}async function jt(e,t,o,n){const i=new FormData;i.set("platform",t);const s=y(o.note,"--note");s!==void 0&&i.set("note",s);const r=y(o.tags,"--tags");return r!==void 0&&i.set("tags",r),i.set("file",await Rt(e),U(e)),await N("/api/v2/builds",{body:i,commandOptions:o,method:"POST",signal:n})}const zt=["platform"],Ft=[{fields:[{header:"ID",key:"id"},{header:"Name",key:"name"},{header:"Platform",key:"platform"},{header:"OS versions",key:"osVersions"}],key:"devices"}];async function Ct({options:e,signal:t}){const o=Mt(e.platform);return{devices:(await Ke(e,t)).filter(i=>o===void 0||i.platform===o).map(Bt)}}function Mt(e){const t=y(e,"--platform");if(t!==void 0){if(!Ge(t))throw new a(`--platform expects one of: ${Ze.join(", ")}`);return t}}function Bt({id:e,name:t,osVersions:o,platform:n}){return{id:e,name:t,osVersions:o,platform:n}}const qt=async({device:e,options:t,positionals:[o]})=>{await e.gesture(Gt(o),Kt(t))};function Kt(e){const t=f(e.timeout,"--timeout");return t===void 0?void 0:{timeout:t}}function Gt(e){let t;try{t=We(e)}catch(o){throw o instanceof SyntaxError?new a(o.message):o}if(t.length===0)throw new a("Expected at least one gesture step");return t}const Zt="APPETIZE_OUTPUT";function Wt(e,t,o){return{format:Xt(e,t),progress:Vt(e,o)}}function C(e,{extension:t,strict:o=!1}){const n=ft(e);if(n==="")return`${e}${t}`;if(o&&n!==t)throw new a(`The output must be a path ending in ${t}`);return e}function Xt(e,t){if(e.json===!0)return"json";if(e.json===!1)return"text";const o=process.env[Zt]?.trim();return o==="json"||o==="text"?o:t.isTTY===!0?"text":"json"}function Vt(e,t){return e.quiet===!0?"silent":t.isTTY===!0&&process.env.TERM!=="dumb"&&!("CI"in process.env)?"interactive":"silent"}const S=["text","testId"],h="select",E={testId:{help:"Test id to match by exact string",suffix:"test-id"},text:{help:'Text to match by exact string or regex (e.g., "Sign in" or "/sign in/i")',suffix:"text"}},M="index",O="position",B="x",q="y";function he(e,{prefix:t=h}={}){return S.reduce((o,n)=>{const{help:i,suffix:s}=E[n];return o.option(`--${l(t,s)}`,i)},e).option(`--${l(t,M)}`,"Which match when multiple (0-based)")}function K(e,t={}){const{prefix:o=h}=t;return he(e,t).option(`--${l(o,O)}`,"Screen position to use as x,y (0-1), e.g., 0.5,0.75").option(`--${l(o,B)}`,"Horizontal position within the element (0-1, outside allowed)").option(`--${l(o,q)}`,"Vertical position within the element (0-1, outside allowed)")}function we(e,{prefix:t=h}={}){const o=Z(e,t);if(o.length===0)throw new a(`A selector is required: pass one of ${$(S,t)}`);if(o.length>1)throw new a(`Only one selector may be given, got ${$(o,t)}`);const[n]=o,i=be(e,l(t,E[n].suffix)),s=f(e[l(t,M)],`--${l(t,M)}`);switch(n){case"testId":return{by:"testId",index:s,testId:i};case"text":return{by:"text",index:s,text:Yt(i)}}}function ge(e){const t=f(e.timeout,"--timeout");return t===void 0?void 0:{timeout:t}}function Ht(e,{prefix:t=h}={}){return xe(e,t)?we(e,{prefix:t}):void 0}function k(e,{prefix:t=h}={}){return xe(e,t)?ye(e,{prefix:t}):void 0}function ye(e,{prefix:t=h}={}){const o=l(t,O),n=e[o],i=Se(e,l(t,B)),s=Se(e,l(t,q));if(n===void 0){if(Z(e,t).length===0)throw new a(`A selector is required: pass one of ${I({prefix:t})}`);return{...we(e,{prefix:t}),x:i,y:s}}if(Z(e,t).length>0)throw new a(`--${o} may not be combined with ${$(S,t)}`);if(i!==void 0||s!==void 0)throw new a(`--${o} may not be combined with --${l(t,B)} or --${l(t,q)}`);return Jt(be(e,o),o)}function I(e={}){const{prefix:t=h}=e;return`${$(S,t)}, --${l(t,O)}`}function G(e={}){const{prefix:t=h}=e;return[...ve(e),l(t,O)]}function ve({prefix:e=h}={}){return S.map(t=>l(e,E[t].suffix))}function Z(e,t){return S.filter(o=>e[l(t,E[o].suffix)]!==void 0)}function l(...e){return e.filter(Boolean).join("-")}function $(e,t){return e.map(o=>`--${l(t,E[o].suffix)}`).join(", ")}function xe(e,t){return Object.entries(e).some(([o,n])=>n!==void 0&&o.startsWith(`${t}-`))}function Se(e,t){return f(e[t],`--${t}`)}function Jt(e,t){const o=e.split(",");if(o.length!==2)throw new a(`--${t} expects x,y (0-1), e.g., 0.5,0.75`);const[n,i]=o.map(s=>Number(s.trim()));return{by:"position",x:Q(n,`--${t}`),y:Q(i,`--${t}`)}}function Yt(e){const t=/^\/(.*)\/([a-z]*)$/s.exec(e);return t?new RegExp(t[1],t[2]):e}function be(e,t){return T(e[t],`--${t}`)}const Qt=ve(),eo=async({device:e,options:t,positionals:[o]})=>{const n=to(o),i=Ht(t),s=await e.inspect(i,ge(t)),r=oo(s,t.pretty===!0);await ce(n,r);const p=Buffer.byteLength(r),d=Ee(s.roots);return i?{bytes:p,matches:s.roots.length,nodes:d,output:n}:{bytes:p,nodes:d,output:n}};function Ee(e){return e.reduce((t,o)=>t+1+Ee(o.children),0)}function to(e="inspect"){return C(e,{extension:".json"})}function oo(e,t){return`${JSON.stringify(e,null,t?2:void 0)}
3
- `}const no=async({device:e})=>{await e.clearKeychain()},io=async({device:e,positionals:[t]})=>{await e.open(so(t))};function so(e){if(typeof e=="string")try{return new URL(e),e}catch{throw new a("<url> expects a URL, e.g., myapp://orders/42")}throw new a("<url> expects a URL, e.g., myapp://orders/42")}const ro=new Set(ue),ao=async({device:e,positionals:[t]})=>{if(!co(t))throw new a(`Button must be one of: ${ue.join(", ")}`);await e.press(t)};function co(e){return typeof e=="string"&&ro.has(e)}const po=async({control:e,options:t,positionals:[o]})=>{const{path:n}=await e.recordStart({force:t.force===!0,path:uo(o)});return{path:n}},lo=async({control:e})=>{const{path:t}=await e.recordStop();return{path:t}};function uo(e){const t=ae(C(e,{extension:".mp4",strict:!0})),o=mt(t);if(!re(o))throw new a(`No such directory: ${o}`);return t}const fo=async({device:e,positionals:[t]})=>{if(!mo(t))throw new a(`Orientation must be one of: ${ee.join(", ")}`);await e.rotate(t)};function mo(e){return ee.some(t=>t===e)}const ho=async({device:e,positionals:[t]})=>{const o=await e.screenshot(),n=go(o.mimeType,t);return await ce(n,o.data),{bytes:o.data.byteLength,output:n}};function wo(e){const t=e.split("/").at(1);return`.${t==="jpeg"?"jpg":t??"png"}`}function go(e,t="screenshot"){return C(t,{extension:wo(e)})}function yo(e){const t=pe(new URL("./daemon.js",import.meta.url)),o=lt(Xe(e.sessionId),"a"),n=vt(process.execPath,[t],{detached:!0,env:{...process.env,[Ve]:He(e),DEBUG:vo(process.env.DEBUG)},stdio:["ignore",o,o]});if(ut(o),n.unref(),n.pid===void 0)throw new Error("Failed to spawn the session daemon");return n.pid}function vo(e){return e?`${e},appetize:daemon`:"appetize:daemon"}const xo=/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/i;function So(){return xt({capitalize:!1,separator:"-"})}function bo(e){if(!xo.test(e))throw new a('--session-id must use letters, digits, ".", "-" or "_"');return e}const Ie=1e4;function Pe(e){if(!Je(e))return!1;try{process.kill(e.pid,"SIGTERM")}catch{}return!0}async function Ae(e,t){(Pe(e)||Io(e))&&await Eo(e,t)}async function Eo({pid:e,sessionId:t},o){const n=oe(e);try{await _(Ie,AbortSignal.any([o,n]))}catch(i){if(n.aborted||!te(e))return;throw i}throw new R(`Session ${t} did not stop in time`,{sessionId:t})}function Io({endedAt:e,pid:t}){return e!==void 0&&Date.now()-Date.parse(e)<Ie&&te(t)}const Po=["device-os-version","open","proxy","target"],Ao=150,To={downloadingApp:"Downloading the app",installingApp:"Installing the app",launchingApp:"Launching the app",queued:"Queued",ready:"Device ready",requesting:"Requesting a device",starting:"Starting the device"},Oo=[{header:"Session ID",key:"sessionId"},{header:"Web view",key:"viewerUrl"},{format:e=>`adb connect ${e}`,header:"adb",key:"adbSerial"},{header:"Host",key:"baseUrl"},{fields:[{header:"Device",key:"device"},{header:"Daemon",key:"daemon"},{header:"Network",key:"network"}],header:"Logs",key:"logs"},{hidden:!0,key:"controlSocket"}];async function ko(e,{onPhase:t,signal:o}){let n;for(;;){const i=ne(e),s=i?.state?.phase;if(s&&s!==n&&(n=s,t?.(s)),i&&s==="ready"&&i.controlSocket!==void 0)return{...i,controlSocket:i.controlSocket};if(s==="error")throw new Error(i?.endedReason??"The session failed to start");if(s==="ended")throw new Error(i?.endedReason??"The session ended");await _(Ao,o)}}function $o(e){const t=y(e,"--open");if(t!==void 0){try{new URL(t)}catch{throw new a("--open expects a URL, e.g., myapp://orders/42")}return t}}function Lo(e){if(!(e===void 0||e===!1)){if(e===""||e==="intercept")return"intercept";if(typeof e!="string"||!No(e))throw new a("--proxy expects 'intercept' or an http:// url, e.g., http://proxy:8080");return e}}async function Do({options:e,positionals:t,progress:o,signal:n}){const i=y(e["device-os-version"],"--device-os-version"),[s]=t,r=y(e.target,"--target")??Ro(e);if(r===void 0&&!process.env.APPETIZE_API_TOKEN)throw new a("APPETIZE_API_TOKEN must be set without a target",["Set APPETIZE_API_TOKEN, or pass --target"]);const p=y(e["session-id"],"--session-id"),d=e.force===!0;if(d&&p===void 0)throw new a("--force needs --session-id");const c=p===void 0?So():bo(p),u={device:i===void 0?{id:s}:{id:s,osVersion:i},endpoint:Qe(e),featureFlags:Ye(),launch:Uo($o(e.open)),proxy:Lo(e.proxy),sessionId:c,target:r},v=ne(c);if(v!==void 0){if(!d)throw new et(c);o.status(`Replacing session ${c}`),await Ae(v,n),tt(c)}yo(u),o.step(`Session ${c}`);const Y=ot(c,u.proxy);if(e.wait===!1)return{logs:Y,sessionId:c};const P=await ko(c,{onPhase:A=>{_o(A)&&o.status(To[A]??"Starting the session")},signal:n}).catch(A=>{throw R.wrap(A,c)});return{adbSerial:P.adbSerial,baseUrl:P.baseUrl,controlSocket:P.controlSocket,logs:Y,sessionId:c,viewerUrl:P.viewerUrl}}function No(e){return e.startsWith("http://")}function _o(e){return e!=="ended"&&e!=="error"}function Ro(e){const t=e._;return Array.isArray(t)&&typeof t[0]=="string"?t[0]:void 0}function Uo(e){return e===void 0?void 0:{url:e}}const jo=async({options:e,progress:t,record:o,signal:n})=>(e.wait===!1?Pe(o):(t.status(`Stopping session ${o.sessionId}`),await Ae(o,n)),{sessionId:o.sessionId}),Te="appetize",zo=pe(new URL(`./skills/${Te}/`,import.meta.url)),W=["agents","claude","codex","copilot","cursor"],Oe=["project","user"],Fo={agents:".agents",claude:".claude",codex:".codex",copilot:".copilot",cursor:".cursor"},Co=x(".agents","skills"),Mo=["claude","agents"];function Bo(e){return W.some(t=>t===e)}function qo(e){return Oe.some(t=>t===e)}async function Ko(e){const t=e.agents??await Go(e),o=await Promise.all(t.map(n=>ke(n,e)));return[...new Set(o.flat())]}async function ke(e,t){const o=$e(e,t);if(e==="claude")return[x(o,"skills")];const n=x(t.root,Co);return t.scope==="project"?[n]:await Le(o)?[n,x(o,"skills")]:[n]}function $e(e,t){if(e==="claude"&&t.scope==="user"){const o=process.env.CLAUDE_CONFIG_DIR?.trim();if(o)return ae(o)}return x(t.root,Fo[e])}async function Go(e){const o=(await Promise.all(W.map(async n=>await Zo(n,e)?n:void 0))).filter(n=>n!==void 0);return o.length>0?o:Mo}async function Zo(e,t){const o=[$e(e,t),...await ke(e,t)];return(await Promise.all(o.map(i=>Le(i)))).includes(!0)}async function Le(e){try{return(await wt(e)).isDirectory()}catch{return!1}}const Wo=["agent","scope"];async function Xo({options:e,signal:t}){const o=Jo(e.scope),n=Ho(e.agent),i=o==="user"?St():process.cwd(),s=await Ko({agents:n,root:i,scope:o}),r=[];for(const p of s){t.throwIfAborted();const d=x(p,Te);await gt(d,{force:!0,recursive:!0}),await yt(zo,d,{recursive:!0}),r.push(o==="user"?d:ht(i,d))}return{installed:r}}function Vo(e){const t=T(e,"--agent");if(!Bo(t))throw new a(`--agent expects one of ${W.join(", ")}`);return t}function Ho(e){return e===void 0?void 0:(Array.isArray(e)?e:[e]).map(o=>Vo(o))}function Jo(e){if(e===void 0)return"project";const t=T(e,"--scope");if(!qo(t))throw new a(`--scope expects one of ${Oe.join(", ")}`);return t}const w="from",g="to",Yo=[...G({prefix:w}),...G({prefix:g})],Qo=async({device:e,options:t})=>{await e.swipe(nn(t),rn(t))};function en(e){return j.some(t=>t===e)}function tn(e){if(e===void 0)return;const t=T(e,"--direction");if(!en(t))throw new a(`--direction expects one of ${j.join(", ")}`);return t}function on(e,t){const o=nt(e.distance,"--distance");if(k(e,{prefix:g})!==void 0)throw new a(`--direction may not be combined with --${g}-*`);return{direction:t,distance:o,from:k(e,{prefix:w})}}function nn(e){const t=tn(e.direction);return t===void 0?sn(e):on(e,t)}function sn(e){if(e.distance!==void 0)throw new a("--distance is only valid with --direction");return{from:De(k(e,{prefix:w}),w),to:De(k(e,{prefix:g}),g)}}function rn(e){const t=f(e.duration,"--duration"),o=f(e.timeout,"--timeout");return t===void 0&&o===void 0?void 0:{duration:t,timeout:o}}function De(e,t){if(e===void 0)throw new a(`A swipe needs --direction, or both endpoints: pass one of ${I({prefix:t})}`);return e}const an=G(),cn=async({device:e,options:t})=>{const o=ye(t);await e.tap(o,ge(t))},pn=async({device:e,options:t,positionals:[o]})=>{await e.type(o,dn(t))};function dn(e){const t=f(e.delay,"--delay");return t===void 0?void 0:{delay:t}}const X=bt("appetize:cli"),ln={fail:()=>{},status:()=>{},step:()=>{},succeed:()=>{}};function un(e,t,o){return e==="interactive"?fn(t,o):ln}function fn(e,t){const o=Et({handleSignals:!1,stream:e});let n=0;const i=()=>{o.stop()};t.addEventListener("abort",i,{once:!0});const s=()=>(process.off("exit",i),o.isSpinning);return{fail:()=>{s()&&o.stop()},status:r=>{if(!t.aborted){if(o.isSpinning){o.text=r;return}n=Date.now(),process.once("exit",i),o.start(r)}},step:r=>{e.write(`\u25B8 ${r}
4
- `)},succeed:()=>{if(!s())return;const r=Math.round((Date.now()-n)/1e3);o.success(`${o.text} (${String(r)}s)`)}}}const mn=new Set(["adb","api","apk","id","os","url"]),hn={bottom:"","bottom-left":"","bottom-mid":"","bottom-right":"",left:"","left-mid":"",mid:"","mid-mid":"",middle:" ",right:"","right-mid":"",top:"","top-left":"","top-mid":"","top-right":""},Ne=2,wn=10;function gn(e){return e.split(/(?=[A-Z])/).map((t,o)=>{const n=t.toLowerCase();return mn.has(n)?n.toUpperCase():o===0?n.charAt(0).toUpperCase()+n.slice(1):n}).join(" ")}function yn(e,t,o){return H(e)?Fe(e,t,o):L(e,{fields:t,key:""},o)}function vn(e,t){const o=[];for(let n=0;n<e.length;n+=t)o.push(e.slice(n,n+t));return o.length===0?[e]:o}function _e(e){return new It({chars:hn,head:e,style:{border:[],head:[],"padding-left":0,"padding-right":Ne}})}function Re(e,t){return e.find(o=>o.key===t)}function Ue(e,t){return t?.header??gn(e)}function V(e){return e!=null}function H(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function je(e){return["boolean","number","string"].includes(typeof e)}function ze(e,t){const o=new Set(t),n=e.map(s=>s.key).filter(s=>o.has(s)),i=new Set(e.filter(s=>s.hidden).map(s=>s.key));return[...new Set([...n,...t])].filter(s=>!i.has(s))}function Fe(e,t,o){const n=new Map(Object.entries(e)),i=ze(t,[...n.keys()]).filter(u=>V(n.get(u))),s=i.map(u=>Re(t,u)),r=i.map((u,v)=>Ue(u,s[v])),p=Math.max(0,...r.map(u=>u.length))+Ne,d=o===void 0?void 0:o-p-1,c=_e();return c.push(...i.map((u,v)=>[r[v],L(n.get(u),s[v],d)])),Ce(c.toString())}function xn(e,t){const o=e.map(r=>new Map(Object.entries(r))),n=ze(t,o.flatMap(r=>[...r.keys()])),i=n.map(r=>Re(t,r)),s=_e(n.map((r,p)=>Ue(r,i[p])));for(const r of o)s.push(n.map((p,d)=>{const c=r.get(p);return V(c)?L(c,i[d]):""}));return Ce(s.toString())}function L(e,t,o){if(t?.format)return D(t.format(e),o);if(je(e))return D(ie(e),o);if(Array.isArray(e)){const n=e.filter(V);return n.every(je)?D(ie(n),o):n.every(H)?xn(n,t?.fields??[]):n.map(i=>L(i,t,o)).join(`
5
- `)}return H(e)?Fe(e,t?.fields??[],o):D(String(e),o)}function Ce(e){return e.split(`
2
+ import Ye from"sade";import{p as u,a as h,f as R,U as a,d as z,b as Qe,i as et,c as tt,e as nt,r as oe,g as I,h as k,j as ot,D as it,s as st,k as rt,l as ie,m as se,S as U,n as j,o as re,q as F,t as T,u as ae,v as ce,w as pe,x as at,y as ct,z as de,A as pt,B as le,C as dt,E as lt,F as ut,G as ft,H as ue,I as fe,J as mt,K as ht,L as gt,M as wt,N as yt,O as vt}from"./stream-C9RfmvVk.js";import{existsSync as me,openAsBlob as xt,openSync as St,closeSync as bt}from"node:fs";import{basename as C,extname as Et,resolve as he,dirname as Ot,join as x,relative as At}from"node:path";import{writeFile as ge,stat as Pt,rm as It,cp as kt}from"node:fs/promises";import{spawn as Tt}from"node:child_process";import{fileURLToPath as we}from"node:url";import{humanId as $t}from"human-id";import{homedir as Lt}from"node:os";import Dt from"debug";import Nt from"yocto-spinner";import _t from"cli-table3";import"json-rpc-2.0";import"zod/mini";import"stream";import"module";import"node:net";const M=["closed","fullyOpen","partiallyOpen"],ye=["back","menu","unlock","volumeDown","volumeUp"],ve=["home","lock"],xe=[...ve,...ye],B=["down","left","right","up"],Se=[{header:"Target",key:"id"},{header:"App",key:"appId"},{header:"Platform",key:"platform"},{header:"Version",key:"versionName"},{header:"Build",key:"buildNumber"},{header:"Uploaded",key:"created"}];function q({appId:e,buildNumber:t,created:n,id:o,platform:i,versionName:s}){return{appId:e,buildNumber:t,created:n,id:o,platform:i,versionName:s}}const Rt=["app"],zt=10,Ut=[{fields:Se,key:"builds"},{header:"Total",key:"total"},{header:"Next page",key:"nextPage"}];async function jt({options:e,signal:t}){const n=u(e.app,"--app"),o=h(e.limit,"--limit")??zt,i=h(e.page,"--page"),s={limit:String(o)};n!==void 0&&(s.appId=n),i!==void 0&&(s.page=String(i));const r=await R("/api/v2/builds",{commandOptions:e,query:s,signal:t});return{builds:r.builds.map(q),nextPage:r.nextPage,total:r.total}}const Ft=["note","tags"],be=12e4,Ct=2e3,G=[".apk",".apks",".zip",".tar.gz",".tgz"],Mt={".apk":"android",".apks":"android",".tar.gz":"ios",".tgz":"ios",".zip":"ios"},Bt=Se;async function qt({options:e,positionals:[t],progress:n,signal:o}){const i=Zt(t),s=C(t);n.status(`Uploading ${s}`);const r=await Vt(t,i,e,o);if(e.wait!==!0)return n.status(`Uploaded ${s}`),q(r);n.step(`Uploaded ${s}`),n.status("Waiting for build metadata");const p=await Gt(r.id,e,o);return n.status(`Processed ${s}`),q(p)}async function Gt(e,t,n){const o=h(t.timeout,"--timeout")??be,i=Date.now()+o;for(;;){const s=await R(`/api/v2/builds/${e}`,{commandOptions:t,signal:n});if(s.appId!==void 0)return s;if(Date.now()>=i)throw new Error(`Build ${e} is still being processed after ${String(o)}ms; find it with 'appetize build list'`);await z(Ct,n)}}async function Kt(e){if(!me(e))throw new Error(`No build file at ${e}`);return await xt(e)}function Zt(e){const t=e.toLowerCase(),n=G.find(o=>t.endsWith(o));if(n===void 0)throw new a(`Cannot tell the platform of ${C(e)}; upload a ${G.join(", ")} file`);return Mt[n]}async function Vt(e,t,n,o){const i=new FormData;i.set("platform",t);const s=u(n.note,"--note");s!==void 0&&i.set("note",s);const r=u(n.tags,"--tags");return r!==void 0&&i.set("tags",r),i.set("file",await Kt(e),C(e)),await R("/api/v2/builds",{body:i,commandOptions:n,method:"POST",signal:o})}const Wt=["platform"],Xt=[{fields:[{header:"ID",key:"id"},{header:"Name",key:"name"},{header:"Platform",key:"platform"},{header:"OS versions",key:"osVersions"}],key:"devices"}];async function Ht({options:e,signal:t}){const n=Jt(e.platform);return{devices:(await Qe(e,t)).filter(i=>n===void 0||i.platform===n).map(Yt)}}function Jt(e){const t=u(e,"--platform");if(t!==void 0){if(!et(t))throw new a(`--platform expects one of: ${tt.join(", ")}`);return t}}function Yt({id:e,name:t,osVersions:n,platform:o}){return{id:e,name:t,osVersions:n,platform:o}}const Qt="cli-fold",en=async({device:e,positionals:[t]})=>{if(!tn(t))throw new a(`Hinge must be one of: ${M.join(", ")}`);await e.fold(t)};function tn(e){return M.some(t=>t===e)}const nn=async({device:e,options:t,positionals:[n]})=>{await e.gesture(sn(n),on(t))};function on(e){const t=h(e.timeout,"--timeout");return t===void 0?void 0:{timeout:t}}function sn(e){let t;try{t=nt(e)}catch(n){throw n instanceof SyntaxError?new a(n.message):n}if(t.length===0)throw new a("Expected at least one gesture step");return t}const rn="APPETIZE_OUTPUT";function an(e,t,n){return{format:cn(e,t),progress:pn(e,n)}}function K(e,{extension:t,strict:n=!1}){const o=Et(e);if(o==="")return`${e}${t}`;if(n&&o!==t)throw new a(`The output must be a path ending in ${t}`);return e}function cn(e,t){if(e.json===!0)return"json";if(e.json===!1)return"text";const n=process.env[rn]?.trim();return n==="json"||n==="text"?n:t.isTTY===!0?"text":"json"}function pn(e,t){return e.quiet===!0?"silent":t.isTTY===!0&&process.env.TERM!=="dumb"&&!("CI"in process.env)?"interactive":"silent"}const S=["text","testId"],g="select",E={testId:{help:"Test id to match by exact string",suffix:"test-id"},text:{help:'Text to match by exact string or regex (e.g., "Sign in" or "/sign in/i")',suffix:"text"}},Z="index",$="position",V="x",W="y";function Ee(e,{prefix:t=g}={}){return S.reduce((n,o)=>{const{help:i,suffix:s}=E[o];return n.option(`--${d(t,s)}`,i)},e).option(`--${d(t,Z)}`,"Which match when multiple (0-based)")}function X(e,t={}){const{prefix:n=g}=t;return Ee(e,t).option(`--${d(n,$)}`,"Screen position to use as x,y (0-1), e.g., 0.5,0.75").option(`--${d(n,V)}`,"Horizontal position within the element (0-1, outside allowed)").option(`--${d(n,W)}`,"Vertical position within the element (0-1, outside allowed)")}function Oe(e,{prefix:t=g}={}){const n=J(e,t);if(n.length===0)throw new a(`A selector is required: pass one of ${D(S,t)}`);if(n.length>1)throw new a(`Only one selector may be given, got ${D(n,t)}`);const[o]=n,i=$e(e,d(t,E[o].suffix)),s=h(e[d(t,Z)],`--${d(t,Z)}`);switch(o){case"testId":return{by:"testId",index:s,testId:i};case"text":return{by:"text",index:s,text:un(i)}}}function Ae(e){const t=h(e.timeout,"--timeout");return t===void 0?void 0:{timeout:t}}function dn(e,{prefix:t=g}={}){return ke(e,t)?Oe(e,{prefix:t}):void 0}function L(e,{prefix:t=g}={}){return ke(e,t)?Pe(e,{prefix:t}):void 0}function Pe(e,{prefix:t=g}={}){const n=d(t,$),o=e[n],i=Te(e,d(t,V)),s=Te(e,d(t,W));if(o===void 0){if(J(e,t).length===0)throw new a(`A selector is required: pass one of ${O({prefix:t})}`);return{...Oe(e,{prefix:t}),x:i,y:s}}if(J(e,t).length>0)throw new a(`--${n} may not be combined with ${D(S,t)}`);if(i!==void 0||s!==void 0)throw new a(`--${n} may not be combined with --${d(t,V)} or --${d(t,W)}`);return ln($e(e,n),n)}function O(e={}){const{prefix:t=g}=e;return`${D(S,t)}, --${d(t,$)}`}function H(e={}){const{prefix:t=g}=e;return[...Ie(e),d(t,$)]}function Ie({prefix:e=g}={}){return S.map(t=>d(e,E[t].suffix))}function J(e,t){return S.filter(n=>e[d(t,E[n].suffix)]!==void 0)}function d(...e){return e.filter(Boolean).join("-")}function D(e,t){return e.map(n=>`--${d(t,E[n].suffix)}`).join(", ")}function ke(e,t){return Object.entries(e).some(([n,o])=>o!==void 0&&n.startsWith(`${t}-`))}function Te(e,t){return h(e[t],`--${t}`)}function ln(e,t){const n=e.split(",");if(n.length!==2)throw new a(`--${t} expects x,y (0-1), e.g., 0.5,0.75`);const[o,i]=n.map(s=>Number(s.trim()));return{by:"position",x:oe(o,`--${t}`),y:oe(i,`--${t}`)}}function un(e){const t=/^\/(.*)\/([a-z]*)$/s.exec(e);return t?new RegExp(t[1],t[2]):e}function $e(e,t){return I(e[t],`--${t}`)}const fn=Ie(),mn=async({device:e,options:t,positionals:[n]})=>{const o=hn(n),i=dn(t),s=await e.inspect(i,Ae(t)),r=gn(s,t.pretty===!0);await ge(o,r);const p=Buffer.byteLength(r),c=Le(s.roots);return i?{bytes:p,matches:s.roots.length,nodes:c,output:o}:{bytes:p,nodes:c,output:o}};function Le(e){return e.reduce((t,n)=>t+1+Le(n.children),0)}function hn(e="inspect"){return K(e,{extension:".json"})}function gn(e,t){return`${JSON.stringify(e,null,t?2:void 0)}
3
+ `}const wn=async({device:e})=>{await e.clearKeychain()},yn=async({device:e,positionals:[t]})=>{await e.open(vn(t))};function vn(e){if(typeof e=="string")try{return new URL(e),e}catch{throw new a("<url> expects a URL, e.g., myapp://orders/42")}throw new a("<url> expects a URL, e.g., myapp://orders/42")}const xn=new Set(xe),Sn=async({device:e,positionals:[t]})=>{if(!bn(t))throw new a(`Button must be one of: ${xe.join(", ")}`);await e.press(t)};function bn(e){return typeof e=="string"&&xn.has(e)}const En=async({control:e,options:t,positionals:[n]})=>{const{path:o}=await e.recordStart({force:t.force===!0,path:An(n)});return{path:o}},On=async({control:e})=>{const{path:t}=await e.recordStop();return{path:t}};function An(e){const t=he(K(e,{extension:".mp4",strict:!0})),n=Ot(t);if(!me(n))throw new a(`No such directory: ${n}`);return t}const Pn=async({device:e,positionals:[t]})=>{if(!In(t))throw new a(`Orientation must be one of: ${k.join(", ")}`);await e.rotate(t)};function In(e){return k.some(t=>t===e)}const kn=async({device:e,positionals:[t]})=>{const n=await e.screenshot(),o=$n(n.mimeType,t);return await ge(o,n.data),{bytes:n.data.byteLength,output:o}};function Tn(e){const t=e.split("/").at(1);return`.${t==="jpeg"?"jpg":t??"png"}`}function $n(e,t="screenshot"){return K(t,{extension:Tn(e)})}function Ln(e){const t=we(new URL("./daemon.js",import.meta.url)),n=St(ot(e.sessionId),"a"),o=Tt(process.execPath,[t],{detached:!0,env:{...process.env,[it]:st(e),DEBUG:Dn(process.env.DEBUG)},stdio:["ignore",n,n]});if(bt(n),o.unref(),o.pid===void 0)throw new Error("Failed to spawn the session daemon");return o.pid}function Dn(e){return e?`${e},appetize:daemon`:"appetize:daemon"}const Nn=/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/i;function _n(){return $t({capitalize:!1,separator:"-"})}function Rn(e){if(!Nn.test(e))throw new a('--session-id must use letters, digits, ".", "-" or "_"');return e}const De=1e4;function Ne(e){if(!rt(e))return!1;try{process.kill(e.pid,"SIGTERM")}catch{}return!0}async function _e(e,t){(Ne(e)||Un(e))&&await zn(e,t)}async function zn({pid:e,sessionId:t},n){const o=se(e);try{await z(De,AbortSignal.any([n,o]))}catch(i){if(o.aborted||!ie(e))return;throw i}throw new U(`Session ${t} did not stop in time`,{sessionId:t})}function Un({endedAt:e,pid:t}){return e!==void 0&&Date.now()-Date.parse(e)<De&&ie(t)}const jn=["device-android-shell","device-appearance","device-font-size","device-ios-keyboards","device-locale","device-location","device-orientation","device-os-version","device-timezone"];function Fn(e,t){const n=j({shell:u(e["device-android-shell"],"--device-android-shell"),softKeyboard:re(e["device-android-soft-keyboard"],{defaultValue:!0})}),o=j({autocorrect:re(e["device-ios-autocorrect"],{defaultValue:!0}),keyboards:F(e["device-ios-keyboards"],"--device-ios-keyboards")});return{android:n,appearance:T(e["device-appearance"],ce,"--device-appearance"),fontSize:T(e["device-font-size"],ae,"--device-font-size"),id:t,ios:o,locale:u(e["device-locale"],"--device-locale"),location:Mn(e["device-location"]),orientation:T(e["device-orientation"],k,"--device-orientation"),osVersion:u(e["device-os-version"],"--device-os-version"),timezone:u(e["device-timezone"],"--device-timezone")}}function Cn(e){return e!==""&&Number.isFinite(Number(e))}function Mn(e){const t=u(e,"--device-location");if(t===void 0)return;const n=t.split(",").map(s=>s.trim());if(n.length!==2||!n.every(Cn))throw new a("--device-location expects <lat>,<lng>, e.g., 43.65,-79.38");const[o,i]=n.map(Number);return{latitude:o,longitude:i}}const Bn=["launch-app","launch-ios-args","launch-params","launch-permissions","launch-url","open"];function qn(e){const t=F(e["launch-ios-args"],"--launch-ios-args");return j({app:Gn(e["launch-app"]),ios:t===void 0?void 0:{args:t},params:Kn(e["launch-params"]),permissions:T(e["launch-permissions"],pe,"--launch-permissions"),url:u(e["launch-url"],"--launch-url")??u(e.open,"--open")})}function Gn(e){return e===!1?!1:u(e,"--launch-app")}function Kn(e){const t=F(e,"--launch-params");if(t!==void 0)return Object.fromEntries(t.map(Zn))}function Zn(e){const t=e.indexOf("=");if(t<1)throw new a("--launch-params expects key=value, e.g., --launch-params debug=true");return[e.slice(0,t),Vn(e.slice(t+1))]}function Vn(e){try{return JSON.parse(e)}catch{return e}}const Re={"device.id":"<device>","device.location.latitude":"--device-location latitude","device.location.longitude":"--device-location longitude"};function Wn(e,{endpointFromEnv:t=!1}={}){const n=t?{...Re,endpoint:"APPETIZE_ENDPOINT"}:Re;try{at(e)}catch(o){throw o instanceof ct?new a(o.issues.map(i=>Xn(i,n)).join(`
4
+ `)):o}}function Xn({code:e,message:t,path:n},o){const i=Hn(n,o);switch(e){case"invalidValue":return`${i} ${t.replace(/^expected /,"expects ")}`;case"needsLaunchApp":return`${i} has no effect without --target, or with --no-launch-app`;case"needsTarget":return`${i} has no effect without --target`}}function Hn(e,t){return t[e]??`--${e.split(".").filter(n=>!/^\d+$/.test(n)).map(n=>n.replaceAll(/([A-Z])/g,"-$1").toLowerCase()).join("-")}`}const Jn=[...jn,...Bn,"proxy","target"],Yn=150,Qn={downloadingApp:"Downloading the app",installingApp:"Installing the app",launchingApp:"Launching the app",queued:"Queued",ready:"Device ready",requesting:"Requesting a device",starting:"Starting the device"},eo=[{header:"Session ID",key:"sessionId"},{header:"Web view",key:"viewerUrl"},{format:e=>`adb connect ${e}`,header:"adb",key:"adbSerial"},{header:"Host",key:"baseUrl"},{fields:[{header:"Device",key:"device"},{header:"Daemon",key:"daemon"},{header:"Network",key:"network"}],header:"Logs",key:"logs"},{hidden:!0,key:"controlSocket"}];async function to(e,{onPhase:t,signal:n}){let o;for(;;){const i=le(e),s=i?.state?.phase;if(s&&s!==o&&(o=s,t?.(s)),i&&s==="ready"&&i.controlSocket!==void 0)return{...i,controlSocket:i.controlSocket};if(s==="error")throw new Error(i?.endedReason??"The session failed to start");if(s==="ended")throw new Error(i?.endedReason??"The session ended");await z(Yn,n)}}function no(e){if(!(e===void 0||e===!1)){if(e===""||e==="intercept")return"intercept";if(typeof e!="string"||!io(e))throw new a("--proxy expects 'intercept' or an http:// url, e.g., http://proxy:8080");return e}}async function oo({options:e,positionals:t,progress:n,signal:o}){const[i]=t,s=u(e.target,"--target")??ro(e);if(s===void 0&&!process.env.APPETIZE_API_TOKEN)throw new a("APPETIZE_API_TOKEN must be set without a target",["Set APPETIZE_API_TOKEN, or pass --target"]);const r=u(e["session-id"],"--session-id"),p=e.force===!0;if(p&&r===void 0)throw new a("--force needs --session-id");const c=r===void 0?_n():Rn(r),l={device:Fn(e,i),endpoint:pt(e),featureFlags:de(),launch:qn(e),proxy:no(e.proxy),sessionId:c,target:s};Wn(l,{endpointFromEnv:e.endpoint===void 0});const f=le(c);if(f!==void 0){if(!p)throw new dt(c);n.status(`Replacing session ${c}`),await _e(f,o),lt(c)}Ln(l),n.step(`Session ${c}`);const v=ut(c,l.proxy);if(e.wait===!1)return{logs:v,sessionId:c};const A=await to(c,{onPhase:P=>{so(P)&&n.status(Qn[P]??"Starting the session")},signal:o}).catch(P=>{throw U.wrap(P,c)});return{adbSerial:A.adbSerial,baseUrl:A.baseUrl,controlSocket:A.controlSocket,logs:v,sessionId:c,viewerUrl:A.viewerUrl}}function io(e){return e.startsWith("http://")}function so(e){return e!=="ended"&&e!=="error"}function ro(e){const t=e._;return Array.isArray(t)&&typeof t[0]=="string"?t[0]:void 0}const ao=async({options:e,progress:t,record:n,signal:o})=>(e.wait===!1?Ne(n):(t.status(`Stopping session ${n.sessionId}`),await _e(n,o)),{sessionId:n.sessionId}),ze="appetize",co=we(new URL(`./skills/${ze}/`,import.meta.url)),Y=["agents","claude","codex","copilot","cursor"],Ue=["project","user"],po={agents:".agents",claude:".claude",codex:".codex",copilot:".copilot",cursor:".cursor"},lo=x(".agents","skills"),uo=["claude","agents"];function fo(e){return Y.some(t=>t===e)}function mo(e){return Ue.some(t=>t===e)}async function ho(e){const t=e.agents??await go(e),n=await Promise.all(t.map(o=>je(o,e)));return[...new Set(n.flat())]}async function je(e,t){const n=Fe(e,t);if(e==="claude")return[x(n,"skills")];const o=x(t.root,lo);return t.scope==="project"?[o]:await Ce(n)?[o,x(n,"skills")]:[o]}function Fe(e,t){if(e==="claude"&&t.scope==="user"){const n=process.env.CLAUDE_CONFIG_DIR?.trim();if(n)return he(n)}return x(t.root,po[e])}async function go(e){const n=(await Promise.all(Y.map(async o=>await wo(o,e)?o:void 0))).filter(o=>o!==void 0);return n.length>0?n:uo}async function wo(e,t){const n=[Fe(e,t),...await je(e,t)];return(await Promise.all(n.map(i=>Ce(i)))).includes(!0)}async function Ce(e){try{return(await Pt(e)).isDirectory()}catch{return!1}}const yo=["agent","scope"];async function vo({options:e,signal:t}){const n=bo(e.scope),o=So(e.agent),i=n==="user"?Lt():process.cwd(),s=await ho({agents:o,root:i,scope:n}),r=[];for(const p of s){t.throwIfAborted();const c=x(p,ze);await It(c,{force:!0,recursive:!0}),await kt(co,c,{recursive:!0}),r.push(n==="user"?c:At(i,c))}return{installed:r}}function xo(e){const t=I(e,"--agent");if(!fo(t))throw new a(`--agent expects one of ${Y.join(", ")}`);return t}function So(e){return e===void 0?void 0:(Array.isArray(e)?e:[e]).map(n=>xo(n))}function bo(e){if(e===void 0)return"project";const t=I(e,"--scope");if(!mo(t))throw new a(`--scope expects one of ${Ue.join(", ")}`);return t}const w="from",y="to",Eo=[...H({prefix:w}),...H({prefix:y})],Oo=async({device:e,options:t})=>{await e.swipe(ko(t),$o(t))};function Ao(e){return B.some(t=>t===e)}function Po(e){if(e===void 0)return;const t=I(e,"--direction");if(!Ao(t))throw new a(`--direction expects one of ${B.join(", ")}`);return t}function Io(e,t){const n=ft(e.distance,"--distance");if(L(e,{prefix:y})!==void 0)throw new a(`--direction may not be combined with --${y}-*`);return{direction:t,distance:n,from:L(e,{prefix:w})}}function ko(e){const t=Po(e.direction);return t===void 0?To(e):Io(e,t)}function To(e){if(e.distance!==void 0)throw new a("--distance is only valid with --direction");return{from:Me(L(e,{prefix:w}),w),to:Me(L(e,{prefix:y}),y)}}function $o(e){const t=h(e.duration,"--duration"),n=h(e.timeout,"--timeout");return t===void 0&&n===void 0?void 0:{duration:t,timeout:n}}function Me(e,t){if(e===void 0)throw new a(`A swipe needs --direction, or both endpoints: pass one of ${O({prefix:t})}`);return e}const Lo=H(),Do=async({device:e,options:t})=>{const n=Pe(t);await e.tap(n,Ae(t))},No=async({device:e,options:t,positionals:[n]})=>{await e.type(n,_o(t))};function _o(e){const t=h(e.delay,"--delay");return t===void 0?void 0:{delay:t}}const Q=Dt("appetize:cli"),Ro={fail:()=>{},status:()=>{},step:()=>{},succeed:()=>{}};function zo(e,t,n){return e==="interactive"?Uo(t,n):Ro}function Uo(e,t){const n=Nt({handleSignals:!1,stream:e});let o=0;const i=()=>{n.stop()};t.addEventListener("abort",i,{once:!0});const s=()=>(process.off("exit",i),n.isSpinning);return{fail:()=>{s()&&n.stop()},status:r=>{if(!t.aborted){if(n.isSpinning){n.text=r;return}o=Date.now(),process.once("exit",i),n.start(r)}},step:r=>{e.write(`\u25B8 ${r}
5
+ `)},succeed:()=>{if(!s())return;const r=Math.round((Date.now()-o)/1e3);n.success(`${n.text} (${String(r)}s)`)}}}const jo=new Set(["adb","api","apk","id","os","url"]),Fo={bottom:"","bottom-left":"","bottom-mid":"","bottom-right":"",left:"","left-mid":"",mid:"","mid-mid":"",middle:" ",right:"","right-mid":"",top:"","top-left":"","top-mid":"","top-right":""},Be=2,Co=10;function Mo(e){return e.split(/(?=[A-Z])/).map((t,n)=>{const o=t.toLowerCase();return jo.has(o)?o.toUpperCase():n===0?o.charAt(0).toUpperCase()+o.slice(1):o}).join(" ")}function Bo(e,t,n){return te(e)?We(e,t,n):N(e,{fields:t,key:""},n)}function qo(e,t){const n=[];for(let o=0;o<e.length;o+=t)n.push(e.slice(o,o+t));return n.length===0?[e]:n}function qe(e){return new _t({chars:Fo,head:e,style:{border:[],head:[],"padding-left":0,"padding-right":Be}})}function Ge(e,t){return e.find(n=>n.key===t)}function Ke(e,t){return t?.header??Mo(e)}function ee(e){return e!=null}function te(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Ze(e){return["boolean","number","string"].includes(typeof e)}function Ve(e,t){const n=new Set(t),o=e.map(s=>s.key).filter(s=>n.has(s)),i=new Set(e.filter(s=>s.hidden).map(s=>s.key));return[...new Set([...o,...t])].filter(s=>!i.has(s))}function We(e,t,n){const o=new Map(Object.entries(e)),i=Ve(t,[...o.keys()]).filter(f=>ee(o.get(f))),s=i.map(f=>Ge(t,f)),r=i.map((f,v)=>Ke(f,s[v])),p=Math.max(0,...r.map(f=>f.length))+Be,c=n===void 0?void 0:n-p-1,l=qe();return l.push(...i.map((f,v)=>[r[v],N(o.get(f),s[v],c)])),Xe(l.toString())}function Go(e,t){const n=e.map(r=>new Map(Object.entries(r))),o=Ve(t,n.flatMap(r=>[...r.keys()])),i=o.map(r=>Ge(t,r)),s=qe(o.map((r,p)=>Ke(r,i[p])));for(const r of n)s.push(o.map((p,c)=>{const l=r.get(p);return ee(l)?N(l,i[c]):""}));return Xe(s.toString())}function N(e,t,n){if(t?.format)return _(t.format(e),n);if(Ze(e))return _(ue(e),n);if(Array.isArray(e)){const o=e.filter(ee);return o.every(Ze)?_(ue(o),n):o.every(te)?Go(o,t?.fields??[]):o.map(i=>N(i,t,n)).join(`
6
+ `)}return te(e)?We(e,t?.fields??[],n):_(String(e),n)}function Xe(e){return e.split(`
6
7
  `).map(t=>t.trimEnd()).join(`
7
- `)}function D(e,t){return t===void 0||t<wn?e:e.split(`
8
- `).flatMap(o=>Sn(o,t)).join(`
9
- `)}function Sn(e,t){const o=[];let n="";for(const i of e.split(" ").flatMap(s=>vn(s,t)))n===""?n=i:n.length+1+i.length<=t?n=`${n} ${i}`:(o.push(n),n=i);return o.push(n),o}async function bn(e,t={}){const o=await se(e,{signal:t.signal});return new it(o,{...t,onClose:()=>{o.close(),t.onClose?.()}})}function En(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");return bn(e.controlSocket,{baseUrl:e.baseUrl,signal:t})}async function In(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");const o=await se(e.controlSocket,{signal:t}),{client:n}=st(o,{});return{close:()=>{o.close()},recordStart:async i=>await n.request("appetizeio.cli.record.start",i),recordStop:async()=>await n.request("appetizeio.cli.record.stop")}}function Pn(e){return async({options:t,positionals:o,progress:n,signal:i})=>{const s=t["session-id"],r=rt(typeof s=="string"?s:void 0);try{return await e({options:t,positionals:o,progress:n,record:r,signal:AbortSignal.any([i,oe(r.pid)])})}catch(p){throw R.wrap(p,r.sessionId)}}}function An(e){return async({options:t,positionals:o,progress:n,record:i,signal:s})=>{const r=await En(i,s);try{return await e({device:r,options:t,positionals:o,progress:n,signal:s})}finally{r.close()}}}function Tn(e){return async({options:t,positionals:o,progress:n,record:i,signal:s})=>{const r=await In(i,s);try{return await e({control:r,options:t,positionals:o,progress:n,signal:s})}finally{r.close()}}}const On=["session-id",...Pt,...kt,...zt,...Qt,...Wo,...Po,...Yo,...an],kn=["json","quiet"];function $n(e){const t=qe("appetize").version(e).option("--json","Print the result as JSON").option("--quiet","Hide progress");return b(t,"session start <device>",o=>o.describe(["Start a device session and wait until it is ready","","Environment variables:","APPETIZE_API_TOKEN: Authorizes private APIs and targets; required with no target","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--device-os-version","Device OS version (e.g., 17 for iOS, 14 for Android)").option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--force","Replace a session with the same id",!1).option("--no-wait","Return without waiting for the device session to be ready").option("--open","URL opened once the session is ready").option("--proxy","Route traffic through a proxy: 'intercept' to capture it, or an http url").option("--session-id","Session id; defaults to a generated 3-word id").option("--target","App, build or app group id to run").example("session start iphone15pro --target app_a1b2c3").example("session start iphone15pro").example("session start pixel7 --target app_a1b2c3 --device-os-version 14").example("session start iphone15pro --target app_a1b2c3 --open myapp://orders/42").example("session start iphone15pro --target app_a1b2c3 --session-id demo --force"),Do,{fields:Oo}),J(t,"session stop",o=>o.describe("Stop a device session and await its exit").option("--no-wait","Return without waiting for the session to exit").example("session stop --no-wait"),jo),b(t,"build list",o=>o.describe(["List uploaded builds, printing session start targets","","Environment variables:","APPETIZE_API_TOKEN: Authorizes the listing request","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--app","App id to filter by (bundle or package id)").option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--limit","Builds per page (default 10, max 200)").option("--page","Page of results to fetch (default 1)").example("build list").example("build list --app com.example.app").example("build list --app com.example.app --page 2"),Ot,{fields:Tt}),b(t,"build upload <file>",o=>o.describe([`Upload a build (${F.join(", ")}) as a session start target`,"","Environment variables:","APPETIZE_API_TOKEN: Authorizes the upload","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--note","Note to store with the build").option("--tags","Comma separated tags to store with the build").option("--timeout",`Milliseconds to wait with --wait (default ${String(me)})`).option("--wait","Wait for the app id and version metadata to resolve").example("build upload ./app.apk").example("build upload ./app.zip").example("build upload ./app.apk --wait").example('build upload ./app.apk --tags beta,latest --note "PR 42"'),Nt,{fields:Dt}),b(t,"device list",o=>o.describe(["List device models and their supported OS versions","","Environment variables:","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--platform","Platform to filter by: ios or android").example("device list").example("device list --platform ios"),Ct,{fields:Ft}),m(t,"press <button>",o=>o.describe(`Press a device button: ${le.join(", ")}; Android only: ${de.join(", ")}`).example("press home").example("press volumeUp"),ao),m(t,"rotate <orientation>",o=>o.describe("Rotate the device to an orientation: portrait, landscapeLeft, landscapeRight or upsideDown").example("rotate portrait").example("rotate landscapeLeft"),fo),m(t,"open <url>",o=>o.describe("Open a URL or deep link on the device").example("open myapp://orders/42").example("open https://example.com"),io),m(t,"keychain clear",o=>o.describe("Clear the entire device keychain (iOS only)").example("keychain clear"),no),m(t,"screenshot [output]",o=>o.describe("Take a screenshot of the current device screen").example("screenshot").example("screenshot home-screen").example("screenshot ./shots/checkout.png"),ho),m(t,"tap",o=>K(o.describe(["Tap an element matched by a selector, or a screen position","",`Pick one of: ${I()}`])).option("--timeout","Milliseconds to wait for the element").example("tap --select-text 'Log in'").example("tap --select-test-id save").example("tap --select-text '/^Item \\d+/i' --select-index 2").example("tap --select-position 0.5,0.75").example("tap --select-test-id slider --select-x 0.9").example("tap --select-test-id dialog --select-x=-0.1").example("tap --select-test-id row --select-y=1.2"),cn),m(t,"swipe",o=>K(K(o.describe(["Swipe in a direction, or along a path between two points","",`Pick a --direction with an optional --${w} starting point,`,`or both a --${w}-* and a --${g}-* point to form a path`,"",`Pick one per point: ${I({prefix:w})}`,` ${I({prefix:g})}`]).option("--direction",`Direction to travel: ${j.join(", ")}`).option("--distance","Travel as a fraction of the screen (0-1)"),{prefix:w}),{prefix:g}).option("--duration","Milliseconds the gesture travels for").option("--timeout",`Milliseconds to wait for any --${w}/--${g} elements`).example("swipe --direction up").example("swipe --direction up --distance 0.25").example("swipe --direction left --from-test-id card --from-index 1").example("swipe --from-position 0.5,0.75 --to-position 0.5,0.3").example("swipe --from-text 'Drag me' --from-y 0.9 --to-test-id drop-zone"),Qo),m(t,"gesture <syntax>",o=>o.describe(["Play a single-pointer gesture",""," > press the pointer down"," / lift it"," {wait} pause 400 ms"," {wait=ms} pause ms"," {...} move the pointer:"," byText=Photo to the element with exactly that text"," byText=/photo/i to the element matching the regex",' byText="My photos" quote values with spaces'," byTestId=card to the element with that test id"," index=1 to the second match, from 0"," x=0 y=0 to a point in it; 0-1 spans it"," 0.5,0.6 to a screen position, each 0-1"," x=0.5 y=0.6 the same screen position"," duration=ms glide time while down; default 400","","Single-quote the gesture. A pointer still down at the end lifts.","Double taps need {wait=100} between taps; Android drops faster ones."]).option("--timeout","Milliseconds to wait for any elements").example("gesture '{byText=Photo}>/' # tap").example("gesture '{byText=Photo}>{wait=800}/' # long-press").example("gesture '{0.5,0.5}>/{wait=100}>/' # double tap").example("gesture '{byTestId=card}>{byTestId=drop-zone duration=600}/' # drag").example(`gesture '{byText="My photos" x=0 y=0}>{wait}/' # top-left corner`).example("gesture '{byText=/photo/i index=1}>{wait=800}/' # 2nd match").example("gesture '{byTestId=slider x=0}>{byTestId=slider x=1 duration=1000}/' # slide").example("gesture '{0.5,0.8}>{0.5,0.6}{0.8,0.6}{0.8,0.2}/' # path").example("gesture '{0.2,0.4}>{0.5,0.6 duration=300}/{0.2,0.7}>{0.8,0.7}/' # 2 strokes").example("gesture '{byTestId=card}>{wait=800}/' --timeout 5000 # wait for card"),qt),m(t,"inspect [output]",o=>he(o.describe("Capture the view hierarchy of the screen, or of every element a selector matches")).option("--timeout","Milliseconds to wait for a match").option("--pretty","Pretty-print the JSON",!1).example("inspect").example("inspect --select-text 'Log in'").example("inspect hierarchy.json --pretty"),eo),m(t,"type <text>",o=>o.describe(`Type text; {...} presses a named key: ${pt.join(", ")}`).option("--delay","Milliseconds to wait between steps").example("type 'hello{Enter}'").example("type '{Backspace>16/}'").example("type 'user@example.com{Tab}hunter2{Enter}' --delay 50"),pn),Me(t,"recording start <output>",o=>o.describe("Start recording the device video to an mp4 file").option("--force","Overwrite the file if it already exists",!1).example("recording start login-flow").example("recording start ./captures/checkout.mp4").example("recording start login-flow --force"),po),Me(t,"recording stop",o=>o.describe("Stop recording the device video and close the file").example("recording stop"),lo),b(t,"skill install",o=>o.describe("Install the Appetize agent skill for coding agents").option("--agent","Agent to install for, repeatable; defaults to the agents detected").option("--scope","Install for the project (default) or the user").example("skill install").example("skill install --scope user").example("skill install --agent claude --agent cursor"),Xo),t}function b(e,t,o,n,{fields:i}={}){const s=t.split(" ").filter(r=>!/^[<[]/.test(r)).join(" ");return o(e.command(t)).action(Nn({action:n,fields:i,name:s}))}function J(e,t,o,n){return b(e,t,i=>o(i).option("--session-id","Target session id; defaults to the sole active session"),Pn(n))}function m(e,t,o,n){return J(e,t,o,An(n))}function Me(e,t,o,n){return J(e,t,o,Tn(n))}function Be(e){return`${String(Date.now()-e)}ms`}function Ln(e,t,o){if(e==="json")return JSON.stringify(t===void 0?{}:t,null,2);const n=t===void 0?"":yn(t,o,process.stdout.columns);return n===""?void 0:n}async function Dn({action:e,fields:t=[],name:o},n){const i=Date.now();X(`invoked: ${process.argv.slice(2).join(" ")}`);const s=at(n),{format:r,progress:p}=Wt(s.options,process.stdout,process.stderr),d=un(p,process.stderr,s.signal);try{const c=Ln(r,await e({...s,progress:d}),t);d.succeed(),c!==void 0&&process.stdout.write(`${c}
10
- `),X(`ok in ${Be(i)}`)}catch(c){d.fail(),process.stderr.write(`${ct(r,c,{command:o})}
11
- `),X(`failed after ${Be(i)}:`,c),process.exitCode=1}}function Nn(e){return(...t)=>{Dn(e,t)}}$n("0.23.0").parse(process.argv,{boolean:kn,string:On});
8
+ `)}function _(e,t){return t===void 0||t<Co?e:e.split(`
9
+ `).flatMap(n=>Ko(n,t)).join(`
10
+ `)}function Ko(e,t){const n=[];let o="";for(const i of e.split(" ").flatMap(s=>qo(s,t)))o===""?o=i:o.length+1+i.length<=t?o=`${o} ${i}`:(n.push(o),o=i);return n.push(o),n}async function Zo(e,t={}){const n=await fe(e,{signal:t.signal});return new mt(n,{...t,onClose:()=>{n.close(),t.onClose?.()}})}function Vo(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");return Zo(e.controlSocket,{baseUrl:e.baseUrl,signal:t})}async function Wo(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");const n=await fe(e.controlSocket,{signal:t}),{client:o}=ht(n,{});return{close:()=>{n.close()},recordStart:async i=>await o.request("appetizeio.cli.record.start",i),recordStop:async()=>await o.request("appetizeio.cli.record.stop")}}function Xo(e){return async({options:t,positionals:n,progress:o,signal:i})=>{const s=t["session-id"],r=gt(typeof s=="string"?s:void 0);try{return await e({options:t,positionals:n,progress:o,record:r,signal:AbortSignal.any([i,se(r.pid)])})}catch(p){throw U.wrap(p,r.sessionId)}}}function Ho(e){return async({options:t,positionals:n,progress:o,record:i,signal:s})=>{const r=await Vo(i,s);try{return await e({device:r,options:t,positionals:n,progress:o,signal:s})}finally{r.close()}}}function Jo(e){return async({options:t,positionals:n,progress:o,record:i,signal:s})=>{const r=await Wo(i,s);try{return await e({control:r,options:t,positionals:n,progress:o,signal:s})}finally{r.close()}}}const Yo=["session-id",...Rt,...Ft,...Wt,...fn,...yo,...Jn,...Eo,...Lo],Qo=["json","quiet"];function ei(e){const t=Ye("appetize").version(e).option("--json","Print the result as JSON").option("--quiet","Hide progress");return b(t,"session start <device>",n=>n.describe(["Start a device session and wait until it is ready","","Environment variables:","APPETIZE_API_TOKEN: Authorizes private APIs and targets; required with no target","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--device-android-shell","Android only: adb shell command run at start; needs --target").option("--device-appearance",`Device appearance: ${ce.join(" or ")}`).option("--device-font-size",`Device font size: ${ae.join(", ")}`).option("--device-ios-keyboards","iOS only: keyboard layout (e.g., en_US@sw=QWERTY); repeatable").option("--device-locale","Device locale as a BCP 47 tag (e.g., fr-CA)").option("--device-location","Device location as <lat>,<lng>; use = if latitude is negative").option("--device-orientation",`Initial orientation: ${k.join(", ")}`).option("--device-os-version","Device OS version (e.g., 17 for iOS, 14 for Android)").option("--device-timezone","Device IANA timezone (e.g., America/Toronto)").option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--force","Replace a session with the same id",!1).option("--launch-app","App id to launch instead of the target app; needs --target").option("--launch-ios-args","iOS only: app launch argument, joined with = if it starts with -; repeatable; needs --target").option("--launch-params","App launch parameter as key=value, JSON when valid; repeatable; needs --target").option("--launch-permissions",`Grant app permissions: ${pe.join(", ")}; needs --target`).option("--launch-url","URL opened once the session is ready").option("--no-device-android-soft-keyboard","Android only: hide the on-screen keyboard").option("--no-device-ios-autocorrect","iOS only: keep autocorrect off, its default").option("--no-launch-app","Skip launching the app; needs --target").option("--no-wait","Return without waiting for the device session to be ready").option("--proxy","Route traffic through a proxy: 'intercept' to capture it, or an http url").option("--session-id","Session id; defaults to a generated 3-word id").option("--target","App, build or app group id to run").example("session start iphone15pro --target app_a1b2c3").example("session start iphone15pro").example("session start pixel7 --target app_a1b2c3 --device-os-version 14").example("session start iphone15pro --target app_a1b2c3 --launch-url myapp://orders/42").example("session start iphone15pro --target app_a1b2c3 --launch-params debug=true --launch-params retries=3").example(`session start iphone15pro --target app_a1b2c3 --launch-params 'zip="90210"'`).example(`session start iphone15pro --target app_a1b2c3 --launch-params 'tags=["beta","qa"]'`).example("session start iphone15pro --device-orientation landscapeLeft --device-appearance dark").example("session start pixel7 --device-locale fr-CA --device-timezone America/Toronto").example("session start iphone15pro --target app_a1b2c3 --session-id demo --force"),oo,{fields:eo}),ne(t,"session stop",n=>n.describe("Stop a device session and await its exit").option("--no-wait","Return without waiting for the session to exit").example("session stop --no-wait"),ao),b(t,"build list",n=>n.describe(["List uploaded builds, printing session start targets","","Environment variables:","APPETIZE_API_TOKEN: Authorizes the listing request","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--app","App id to filter by (bundle or package id)").option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--limit","Builds per page (default 10, max 200)").option("--page","Page of results to fetch (default 1)").example("build list").example("build list --app com.example.app").example("build list --app com.example.app --page 2"),jt,{fields:Ut}),b(t,"build upload <file>",n=>n.describe([`Upload a build (${G.join(", ")}) as a session start target`,"","Environment variables:","APPETIZE_API_TOKEN: Authorizes the upload","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--note","Note to store with the build").option("--tags","Comma separated tags to store with the build").option("--timeout",`Milliseconds to wait with --wait (default ${String(be)})`).option("--wait","Wait for the app id and version metadata to resolve").example("build upload ./app.apk").example("build upload ./app.zip").example("build upload ./app.apk --wait").example('build upload ./app.apk --tags beta,latest --note "PR 42"'),qt,{fields:Bt}),b(t,"device list",n=>n.describe(["List device models and their supported OS versions","","Environment variables:","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--platform","Platform to filter by: ios or android").example("device list").example("device list --platform ios"),Ht,{fields:Xt}),m(t,"press <button>",n=>n.describe(`Press a device button: ${ve.join(", ")}; Android only: ${ye.join(", ")}`).example("press home").example("press volumeUp"),Sn),m(t,"rotate <orientation>",n=>n.describe("Rotate the device to an orientation: portrait, landscapeLeft, landscapeRight or upsideDown").example("rotate portrait").example("rotate landscapeLeft"),Pn),Qt in de()&&m(t,"fold <hinge>",n=>n.describe(`Fold a foldable device: ${M.join(", ")}`).example("fold closed").example("fold fullyOpen"),en),m(t,"open <url>",n=>n.describe("Open a URL or deep link on the device").example("open myapp://orders/42").example("open https://example.com"),yn),m(t,"keychain clear",n=>n.describe("Clear the entire device keychain (iOS only)").example("keychain clear"),wn),m(t,"screenshot [output]",n=>n.describe("Take a screenshot of the current device screen").example("screenshot").example("screenshot home-screen").example("screenshot ./shots/checkout.png"),kn),m(t,"tap",n=>X(n.describe(["Tap an element matched by a selector, or a screen position","",`Pick one of: ${O()}`])).option("--timeout","Milliseconds to wait for the element").example("tap --select-text 'Log in'").example("tap --select-test-id save").example("tap --select-text '/^Item \\d+/i' --select-index 2").example("tap --select-position 0.5,0.75").example("tap --select-test-id slider --select-x 0.9").example("tap --select-test-id dialog --select-x=-0.1").example("tap --select-test-id row --select-y=1.2"),Do),m(t,"swipe",n=>X(X(n.describe(["Swipe in a direction, or along a path between two points","",`Pick a --direction with an optional --${w} starting point,`,`or both a --${w}-* and a --${y}-* point to form a path`,"",`Pick one per point: ${O({prefix:w})}`,` ${O({prefix:y})}`]).option("--direction",`Direction to travel: ${B.join(", ")}`).option("--distance","Travel as a fraction of the screen (0-1)"),{prefix:w}),{prefix:y}).option("--duration","Milliseconds the gesture travels for").option("--timeout",`Milliseconds to wait for any --${w}/--${y} elements`).example("swipe --direction up").example("swipe --direction up --distance 0.25").example("swipe --direction left --from-test-id card --from-index 1").example("swipe --from-position 0.5,0.75 --to-position 0.5,0.3").example("swipe --from-text 'Drag me' --from-y 0.9 --to-test-id drop-zone"),Oo),m(t,"gesture <syntax>",n=>n.describe(["Play a single-pointer gesture",""," > press the pointer down"," / lift it"," {wait} pause 400 ms"," {wait=ms} pause ms"," {...} move the pointer:"," byText=Photo to the element with exactly that text"," byText=/photo/i to the element matching the regex",' byText="My photos" quote values with spaces'," byTestId=card to the element with that test id"," index=1 to the second match, from 0"," x=0 y=0 to a point in it; 0-1 spans it"," 0.5,0.6 to a screen position, each 0-1"," x=0.5 y=0.6 the same screen position"," duration=ms glide time while down; default 400","","Single-quote the gesture. A pointer still down at the end lifts.","Double taps need {wait=100} between taps; Android drops faster ones."]).option("--timeout","Milliseconds to wait for any elements").example("gesture '{byText=Photo}>/' # tap").example("gesture '{byText=Photo}>{wait=800}/' # long-press").example("gesture '{0.5,0.5}>/{wait=100}>/' # double tap").example("gesture '{byTestId=card}>{byTestId=drop-zone duration=600}/' # drag").example(`gesture '{byText="My photos" x=0 y=0}>{wait}/' # top-left corner`).example("gesture '{byText=/photo/i index=1}>{wait=800}/' # 2nd match").example("gesture '{byTestId=slider x=0}>{byTestId=slider x=1 duration=1000}/' # slide").example("gesture '{0.5,0.8}>{0.5,0.6}{0.8,0.6}{0.8,0.2}/' # path").example("gesture '{0.2,0.4}>{0.5,0.6 duration=300}/{0.2,0.7}>{0.8,0.7}/' # 2 strokes").example("gesture '{byTestId=card}>{wait=800}/' --timeout 5000 # wait for card"),nn),m(t,"inspect [output]",n=>Ee(n.describe("Capture the view hierarchy of the screen, or of every element a selector matches")).option("--timeout","Milliseconds to wait for a match").option("--pretty","Pretty-print the JSON",!1).example("inspect").example("inspect --select-text 'Log in'").example("inspect hierarchy.json --pretty"),mn),m(t,"type <text>",n=>n.describe(`Type text; {...} presses a named key: ${vt.join(", ")}`).option("--delay","Milliseconds to wait between steps").example("type 'hello{Enter}'").example("type '{Backspace>16/}'").example("type 'user@example.com{Tab}hunter2{Enter}' --delay 50"),No),He(t,"recording start <output>",n=>n.describe("Start recording the device video to an mp4 file").option("--force","Overwrite the file if it already exists",!1).example("recording start login-flow").example("recording start ./captures/checkout.mp4").example("recording start login-flow --force"),En),He(t,"recording stop",n=>n.describe("Stop recording the device video and close the file").example("recording stop"),On),b(t,"skill install",n=>n.describe("Install the Appetize agent skill for coding agents").option("--agent","Agent to install for, repeatable; defaults to the agents detected").option("--scope","Install for the project (default) or the user").example("skill install").example("skill install --scope user").example("skill install --agent claude --agent cursor"),vo),t}function b(e,t,n,o,{fields:i}={}){const s=t.split(" ").filter(r=>!/^[<[]/.test(r)).join(" ");return n(e.command(t)).action(oi({action:o,fields:i,name:s}))}function ne(e,t,n,o){return b(e,t,i=>n(i).option("--session-id","Target session id; defaults to the sole active session"),Xo(o))}function m(e,t,n,o){return ne(e,t,n,Ho(o))}function He(e,t,n,o){return ne(e,t,n,Jo(o))}function Je(e){return`${String(Date.now()-e)}ms`}function ti(e,t,n){if(e==="json")return JSON.stringify(t===void 0?{}:t,null,2);const o=t===void 0?"":Bo(t,n,process.stdout.columns);return o===""?void 0:o}async function ni({action:e,fields:t=[],name:n},o){const i=Date.now();Q(`invoked: ${process.argv.slice(2).join(" ")}`);const s=wt(o),{format:r,progress:p}=an(s.options,process.stdout,process.stderr),c=zo(p,process.stderr,s.signal);try{const l=ti(r,await e({...s,progress:c}),t);c.succeed(),l!==void 0&&process.stdout.write(`${l}
11
+ `),Q(`ok in ${Je(i)}`)}catch(l){c.fail(),process.stderr.write(`${yt(r,l,{command:n})}
12
+ `),Q(`failed after ${Je(i)}:`,l),process.exitCode=1}}function oi(e){return(...t)=>{ni(e,t)}}ei("0.25.0").parse(process.argv,{boolean:Qo,string:Yo});
package/daemon.js CHANGED
@@ -1,3 +1,2 @@
1
- import{T as G,G as I,H as K,I as L,J,z as Z,K as B,h as Y,L as X,M as U,N as ee,O as te,v as ne,P as oe,Q as re,R as se,V as ae}from"./stream-DqsoC5l1.js";import k from"debug";import*as a from"zod/mini";import{createServer as x}from"node:net";import{pipeline as C}from"node:stream";import{rm as ie}from"node:fs/promises";import{readFileSync as ce,createWriteStream as de,existsSync as le,rmSync as ue}from"node:fs";import{createServer as pe}from"node:http";import{WebSocketServer as fe}from"ws";import{pipeline as he}from"node:stream/promises";import{EncodedVideoPacketSource as ge,Output as we,FilePathTarget as me,Mp4OutputFormat as ye,EncodedPacket as ve}from"mediabunny";import"json-rpc-2.0";import"node:path";import"node:os";import"stream";import"module";const be=["h264","jpeg"],Se=k("appetize.io:transport:ws-ping"),Ee=3e4;function Ae(e){let t=!0;const n=()=>{t=!0};e.on("pong",n);const o=setInterval(()=>{if(!t){clearInterval(o),Se("liveness ping went unanswered, terminating"),e.terminate();return}e.readyState===e.OPEN&&(t=!1,e.ping())},Ee);return{destroy(){clearInterval(o),e.off("pong",n)},pong:n}}const y=k("appetize.io:trace:transport:websocket");class R extends G{constructor(t,{signal:n}={}){super(async r=>{this.#e.send(I(r))},{signal:n}),this.wsOrUrl=t,typeof t=="string"?(this.#e=new WebSocket(t),this.#e.binaryType="arraybuffer"):this.#e=t;const o=this.#e;if(o instanceof WebSocket?(o.addEventListener("open",this.#s),o.addEventListener("message",this.#r),o.addEventListener("error",this.#o),o.addEventListener("close",this.#n),this.#t.push(()=>{o.removeEventListener("open",this.#s),o.removeEventListener("message",this.#r),o.removeEventListener("error",this.#o),o.removeEventListener("close",this.#n)})):(o.addEventListener("open",this.#s),o.addEventListener("message",this.#r),o.addEventListener("error",this.#o),o.addEventListener("close",this.#n),this.#t.push(()=>{o.removeEventListener("open",this.#s),o.removeEventListener("message",this.#r),o.removeEventListener("error",this.#o),o.removeEventListener("close",this.#n)})),this.send=o.send.bind(o),"ping"in o){const{destroy:r,pong:s}=Ae(o);this.#c=s,this.#t.push(r)}}wsOrUrl;onclose;onerror;onmessage;onopen;send;get bufferedAmount(){return this.#e.bufferedAmount}get readyState(){return this.#e.readyState}#t=[];#a=!1;#i=!1;#c;#e;close({destroyWs:t=!1,...n}={}){this.closed||(this.#a=t,super.close(n))}handleClose(t){y("close()");for(const n of this.#t.splice(0))n();if((typeof this.wsOrUrl=="string"||this.#a)&&(this.#e.close(t.code,t.reason),"terminate"in this.#e)){const n=this.#e;setTimeout(()=>{n.terminate()},100)}this.#d(),super.handleClose(t)}#d(t){this.#i||(this.#i=!0,this.onclose?.(t))}#n=t=>{y("handleWebSocketClose()",t);try{this.#d(t)}finally{this.close()}};#o=t=>{y("handleWebSocketError()",t),this.onerror?.(t)};#r=async t=>{y("handleWebSocketMessage()",t),this.#c?.();const n=this.#l(t);if(!n){y("handleWebSocketMessage() -> legacy"),this.onmessage?.(t);return}y("handleWebSocketMessage() -> jsonrpc",n),this[K].dispatchMessage(n)};#s=t=>{y("handleWebSocketOpen()",t),this.onopen?.(t)};#l(t){try{const n=L(new Uint8Array(t.data));return J(n)?n:null}catch{return null}}}function A(e){return typeof e=="object"&&e!==null}function z(e){try{const t=L(new Uint8Array(e.data));if(A(t)&&typeof t.name=="string")return{name:t.name,payload:"payload"in t?t.payload:void 0}}catch{}}async function Te(e,t={}){const n=t.baseUrl??(typeof e=="string"?new URL(e).origin:void 0),o=typeof e=="string"?new R(e,{signal:t.signal}):e,r=new Z(o,{...t,baseUrl:n});return typeof e=="string"&&await Le(o,r,t),r}function Le(e,t,n){const o=B(n)?.onLegacyMessage;return new Promise((r,s)=>{e.onopen=()=>{r()},e.onclose=()=>{t.close(),n.signal?.aborted?s(n.signal.reason):s(new Error("The websocket was closed before the device connection was established"))},e.onerror=s,e.onmessage=i=>{const d=z(i);d&&o?.(d)}})}async function ke(e,{onStateChange:t,signal:n}){n?.throwIfAborted(),t?.({phase:"starting"}),await new Promise((o,r)=>{const s=()=>{i(),r(n?.reason)},i=()=>{e.onmessage=void 0,e.onclose=void 0,n?.removeEventListener("abort",s)};e.onmessage=d=>{const c=Re(z(d));c&&(t?.(c),c.phase==="ready"&&(i(),o()))},e.onclose=()=>{i(),r(new Error("The device closed before it was ready"))},n?.addEventListener("abort",s,{once:!0})})}function Re(e){if(!e)return;if(e.name==="ready")return{phase:"ready"};if(e.name!=="launchStatus")return;switch(A(e.payload)&&typeof e.payload.status=="string"?e.payload.status:void 0){case"downloadingApp":return{phase:"downloadingApp"};case"installingApp":return{phase:"installingApp"};case"launchingApp":return{phase:"launchingApp"};default:return}}function $e(){return typeof process>"u"?void 0:process}const Pe=["light","dark"],Ie=["xs","s","m","l","xl","xxl","xxxl"];function Ue(e){return{adbShellCommand:e.android?.shell,appearance:e.appearance,deviceType:e.id,disableVirtualKeyboard:e.android?.softKeyboard===!1?!0:void 0,fontSize:e.fontSize,iosAutocorrect:e.ios?.autocorrect,iosKeyboard:e.ios?.keyboards,language:e.locale,locale:e.locale,location:e.location&&[e.location.latitude,e.location.longitude],orientation:e.orientation,osVersion:e.osVersion,timezone:e.timezone}}const xe=["grant"];function Ce(e){return{grantPermissions:e?.permissions==="grant"?!0:void 0,launchApp:e?.app,launchArgs:e?.ios?.args,launchUrl:e?.url,params:e?.params}}function ze(e){return e==="intercept"?"interceptOnDemand":e}class v extends Error{code;issues;constructor(t,n,o=[]){super(n),this.name="SessionRequestError",this.code=t,this.issues=o}}const l=e=>`expected ${e}`,b=e=>a.string(l(e)).check(a.minLength(1,l(e))),E=e=>a.literal(e,l(`one of ${e.join(", ")}`)),S=e=>a.object(e,l("an object")),_=()=>a.boolean(l("true or false")),D=e=>a.number(l("a number")).check(a.gte(-e,l(`at least -${String(e)}`)),a.lte(e,l(`at most ${String(e)}`))),_e=/^\w{2}(?:_\w{2})?@sw(=[\w-]+)?$/,De=/^[a-z0-9_.=:/ ()"-]+$/i,Oe='letters, digits or _ . = : / ( ) " - and spaces',We=/^[A-Za-z]{2,3}([-_][A-Za-z0-9]{2,8})*$/;function Me(e){if(!We.test(e))return!1;try{return new Intl.Locale(e.replaceAll("_","-")),!0}catch{return!1}}function qe(e){try{return new Intl.DateTimeFormat(void 0,{timeZone:e}),!0}catch{return!1}}const $="has no effect without a target",O="has no effect without a target, or with launch.app: false";function W({launch:e}){return e?.app!==!1}const je=a.object({apiToken:a.optional(b("an API token")),codec:a.optional(E(be)),device:S({android:a.optional(S({shell:a.optional(b("a shell command")),softKeyboard:a.optional(_())})),appearance:a.optional(E(Pe)),fontSize:a.optional(E(Ie)),id:b("a device id"),ios:a.optional(S({autocorrect:a.optional(_()),keyboards:a.optional(a.array(a.string(l("a keyboard layout")).check(a.regex(_e,l("a layout like en_US@sw=QWERTY"))),l("a list of keyboard layouts")))})),locale:a.optional(a.string(l("a BCP 47 tag, e.g. fr-CA")).check(a.refine(Me,{error:l("a BCP 47 tag, e.g. fr-CA")}))),location:a.optional(S({latitude:D(90),longitude:D(180)})),orientation:a.optional(E(Y)),osVersion:a.optional(b("an OS version")),timezone:a.optional(a.string(l("an IANA timezone")).check(a.refine(qe,{error:l("an IANA timezone, e.g. America/Toronto")})))}),endpoint:a.optional(a.url(l("a URL"))),launch:a.optional(S({app:a.optional(a.union([b("an app id"),a.literal(!1)],l("an app id or false"))),ios:a.optional(S({args:a.optional(a.array(a.string(l("an argument")).check(a.regex(De,l(Oe))),l("a list of arguments")).check(a.maxLength(20,l("at most 20 arguments"))))})),params:a.optional(a.custom(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),l("an object"))),permissions:a.optional(E(xe)),url:a.optional(a.url(l("a URL")))})),proxy:a.optional(a.union([a.literal("intercept"),a.templateLiteral(["http://",a.string()])],l("intercept or an http:// URL"))),target:a.optional(b("an app, build or group id"))}).check(a.refine(e=>e.device.android?.shell===void 0||e.target!==void 0,{error:$,path:["device","android","shell"]}),a.refine(e=>e.launch?.app===void 0||e.target!==void 0,{error:$,path:["launch","app"]}),a.refine(e=>e.launch?.permissions===void 0||e.target!==void 0,{error:$,path:["launch","permissions"]}),a.refine(e=>e.launch?.params===void 0||e.target!==void 0&&W(e),{error:O,path:["launch","params"]}),a.refine(e=>e.launch?.ios?.args===void 0||e.target!==void 0&&W(e),{error:O,path:["launch","ios","args"]}));function Ne(e){const t=je.safeParse(e);if(t.success)return;const n=t.error.issues.map(o=>({message:o.message,path:o.path.join(".")}));throw new v("invalid",n.map(({message:o,path:r})=>`${r}: ${o}`).join(`
2
- `),n)}function Ve(e,{onClose:t,onEvent:n,signal:o}={}){return new Promise((r,s)=>{const i=new WebSocket(e);i.binaryType="arraybuffer",i.onopen=()=>{M(i,{capabilities:{},t:"handshake"})},i.onmessage=d=>{Fe(i,d,{onEvent:n,onHandshake:c=>{r({close:()=>{i.close()},id:c})}})},i.onclose=()=>{s(new v("disconnected",`The connection to ${e} closed`)),t?.()},o?.addEventListener("abort",()=>{s(o.reason),i.close()},{once:!0})})}function Fe(e,t,n){const o=L(new Uint8Array(t.data));A(o)&&(o.t==="handshake"&&typeof o.id=="string"?n.onHandshake(o.id):o.name==="ping"?M(e,{name:"pong"}):typeof o.name=="string"&&n.onEvent?.(o.name,o.payload))}function M(e,t){e.send(I(t))}async function He(e,t,{apiToken:n,onQueue:o,signal:r}={}){const s=X();Je(s.reject,r);const i=await Ve(Ze(e),{onClose:()=>{s.reject(Qe())},onEvent:(d,c)=>{Ge(d,c,s,o)},signal:r});return Ke(e,{apiToken:n,data:t,socketid:i.id},r).catch(s.reject),await s.promise.finally(()=>{i.close()})}function Qe(){return new v("disconnected","The webserver connection closed before a session was granted")}function q(e,t){return A(e)?e[t]:void 0}function Ge(e,t,n,o){if(e==="queue"||e==="concurrentQueue"){const r=e==="queue"?Ye(t):Xe(t);r&&o?.(r)}else e==="newSession"?Be(n,t):e==="userError"&&n.reject(new v("denied",et(t)))}function j(e,t){const n=q(e,t);return typeof n=="number"?n:void 0}function T(e,t){const n=q(e,t);return typeof n=="string"?n:void 0}async function Ke(e,t,n){const o=await fetch(new URL("/api/session/request",e),{body:JSON.stringify(t),headers:{"content-type":"application/json"},method:"POST",signal:n});if(!o.ok)throw new v("denied",`The webserver rejected the session request (${String(o.status)})`)}function Je(e,t){t?.addEventListener("abort",()=>{e(t.reason)},{once:!0})}function Ze(e){const t=new URL("/client",e);return t.protocol=t.protocol==="http:"?"ws:":"wss:",t.href}function Be(e,t){const n=T(t,"path"),o=T(t,"sessionToken");n!==void 0&&o!==void 0?e.resolve({path:n,sessionToken:o}):e.reject(new v("denied","The granted session is missing its address"))}function Ye(e){const t=j(e,"position");return t===void 0?void 0:{position:t,reason:"capacity"}}function Xe(e){const t=j(e,"position");if(t!==void 0)return{limit:T(e,"name")??"Queue",position:t,reason:"concurrency"}}function et(e){return T(e,"message")??"The session request was denied"}async function tt({apiToken:e=nt(),codec:t="h264",device:n,endpoint:o="https://appetize.io",launch:r,onStateChange:s,proxy:i,signal:d,target:c}){d?.throwIfAborted(),Ne({apiToken:e,codec:t,device:n,endpoint:o,launch:r,proxy:i,target:c}),s?.({phase:"requesting"});const u=await He(o,{codec:t,noVideo:!0,proxy:ze(i),publicKey:c,...Ue(n),...Ce(r)},{apiToken:e,onQueue:h=>{s?.({phase:"queued",queue:h})},signal:d});return{baseUrl:new URL(u.path).origin,url:ot(u)}}function nt(){return $e()?.env.APPETIZE_API_TOKEN}function ot(e){const t=new URL("/client",e.path);return t.protocol=t.protocol==="http:"?"ws:":"wss:",t.searchParams.set("sessionToken",e.sessionToken),t.href}async function rt(e,{host:t="127.0.0.1",onError:n,port:o=0,signal:r}={}){r?.throwIfAborted();let s;const i=()=>{const p=s;s=void 0,p&&(p.tunnel?.close(),p.socket.destroy())},d=async p=>{const{socket:f}=p;try{const w=await e.debug.createAdbTunnel();if(f.destroyed){w.close();return}p.tunnel=w,w.closed.catch(m=>{n?.(m)}),st(f,w)}catch(w){f.destroyed||n?.(w),f.destroy()}},c=x(p=>{i();const f={socket:p};s=f,p.once("close",()=>{s===f&&(s=void 0)}),d(f)}),u=()=>{r?.removeEventListener("abort",u),i(),c.close()};r?.addEventListener("abort",u,{once:!0});const h=await at(c,t,o);return{close:u,url:new URL(`adb://${h.address}:${String(h.port)}`)}}function st(e,t){C(t.readable,e,N),C(e,async n=>{const o=t.writable.getWriter();try{for await(const r of n)await o.write(r);await o.close()}catch(r){throw await o.abort(r),r}},N)}function N(){}function at(e,t,n){return new Promise((o,r)=>{e.once("error",r),e.listen({host:t,port:n},()=>{e.off("error",r);const s=e.address();if(s===null||typeof s=="string"){r(new Error("The listener has no tcp address"));return}o(s)})})}async function it(e,{methods:t={},path:n,signal:o}){o?.throwIfAborted(),await ie(n,{force:!0});const r=new Set,s=x(d=>{r.add(d),d.once("close",()=>{r.delete(d)}),U(new ee(d),e,{leftEffects:()=>t})}),i=()=>{o?.removeEventListener("abort",i);for(const d of r)d.destroy();s.close()};return await ct(s,n),o?.aborted&&(i(),o.throwIfAborted()),o?.addEventListener("abort",i,{once:!0}),{close:i,path:n}}function ct(e,t){return new Promise((n,o)=>{e.once("error",o),e.listen(t,()=>{e.off("error",o),n()})})}const dt=k("appetize:daemon");function g(e){dt(e)}function lt(e,{message:t="The operation timed out"}={}){const n=new AbortController,o=setTimeout(()=>{n.abort(new Error(t))},e),r=()=>{clearTimeout(o)};return{cancel:r,signal:n.signal,[Symbol.dispose]:r}}var ut=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),V=e=>{throw TypeError(e)},pt=(e,t,n)=>{if(t!=null){typeof t!="object"&&typeof t!="function"&&V("Object expected");var o,r;o===void 0&&(o=t[ut("dispose")]),typeof o!="function"&&V("Object not disposable"),r&&(o=function(){try{r.call(this)}catch(s){return Promise.reject(s)}}),e.push([n,o,t])}return t},ft=(e,t,n)=>{var o=typeof SuppressedError=="function"?SuppressedError:function(i,d,c,u){return u=Error(c),u.name="SuppressedError",u.error=i,u.suppressed=d,u},r=i=>t=n?new o(i,t,"An error was suppressed during disposal"):(n=!0,i),s=i=>{for(;i=e.pop();)try{var d=i[1]&&i[1].call(i[2]);if(i[0])return Promise.resolve(d).then(s,c=>(r(c),s()))}catch(c){r(c)}if(n)throw t};return s()};const ht=1e4;async function gt(e,t,{onClose:n,signal:o}={}){var r=[];try{const d=pt(r,lt(ht,{message:"Timed out waiting for session request state"})),c=AbortSignal.any([o,d.signal].filter(m=>!!m)),u=m=>{t.save({state:m}),g(m.phase),m.phase!=="requesting"&&d.cancel()},{baseUrl:h,url:p}=await tt({device:e.device,endpoint:e.endpoint,launch:e.launch,onStateChange:u,proxy:e.proxy,signal:c,target:e.target});t.save({baseUrl:h});const f=new R(p,{signal:c});return await ke(f,{onStateChange:u,signal:c}),f.onclose=()=>{n?.("The device connection closed")},{device:await Te(f,{baseUrl:h,signal:c}),transport:f}}catch(d){var s=d,i=!0}finally{ft(r,s,i)}}function wt(){const e=te(),t=ne(e.sessionId,e.proxy),n=oe({device:e.device,endpoint:e.endpoint,open:e.launch?.url,pid:process.pid,proxy:e.proxy,sessionId:e.sessionId,startedAt:new Date().toISOString(),target:e.target});return n.save({logs:t}),{config:e,logs:t,store:n}}async function mt(e,t,n,{signal:o}={}){try{if(!await yt(t))return;const r=await rt(e,{onError:s=>{g(`adb endpoint error: ${F(s)}`)},signal:o});n.save({adbSerial:r.url.host}),g(`adb endpoint listening at ${r.url.host}`)}catch(r){g(`adb endpoint unavailable: ${F(r)}`)}}async function yt(e){return await re(e.device.id,{endpoint:e.endpoint})==="android"}function F(e){return e instanceof Error?e.message:String(e)}async function vt(e,t,n,{recorder:o,signal:r}){const{path:s}=await it(e,{methods:{"appetizeio.cli.record.start":async i=>await o.start(i),"appetizeio.cli.record.stop":async()=>await o.stop()},path:se(n),signal:r});return t.save({controlSocket:s}),s}const H="127.0.0.1";async function bt(e,t,n={}){try{const o=await St(e,n);return t.save({viewerUrl:o.url.href}),g(`web endpoint listening at ${o.url.href}`),o}catch(o){g(`web endpoint unavailable: ${kt(o)}`);return}}async function St(e,{port:t=0,signal:n,viewerHtml:o=ce(new URL("./viewer.html",import.meta.url),"utf8")}){n?.throwIfAborted();const r=pe((p,f)=>{At(p,f,o)}),s=new Set,i=new fe({noServer:!0,perMessageDeflate:!1});let d;i.on("connection",p=>{if(s.size>0){p.close(1013,"Viewer already connected");return}s.add(p);const f=new R(p);U(f,e),p.once("close",()=>{s.delete(p)})}),r.on("upgrade",(p,f,w)=>{Tt(i,p,f,w)});const c=()=>(d??=new Promise((p,f)=>{n?.removeEventListener("abort",u);for(const w of s)Et(w);i.close(),r.close(w=>{w?f(w):p()})}),d),u=()=>{c()};await Lt(r,t),n?.addEventListener("abort",u,{once:!0});const h=r.address();if(!h||typeof h=="string")throw await c(),new Error("Web address unavailable");return{close:c,url:new URL(`http://${H}:${String(h.port)}/`)}}function Et(e){e.close(1001)}function At(e,t,n){if(e.method==="GET"&&e.url==="/"){t.writeHead(200,{"Cache-Control":"no-store","Content-Type":"text/html; charset=utf-8"}),t.end(n);return}t.writeHead(404).end()}function Tt(e,t,n,o){if(t.url!=="/ws"){n.destroy();return}e.handleUpgrade(t,n,o,r=>{e.emit("connection",r,t)})}function Lt(e,t){return new Promise((n,o)=>{const r=s=>{o(s)};e.once("error",r),e.listen(t,H,()=>{e.removeListener("error",r),n()})})}function kt(e){return e instanceof Error?e.message:String(e)}function Rt(e){const t=new AbortController,n=$t(e,t),o=ae();return o.addEventListener("abort",()=>{const{reason:r}=o;n(r instanceof Error?r.message:String(r))},{once:!0}),{shutdown:n,signal:t.signal}}function $t(e,t){let n=!1;return o=>{n||(n=!0,e.save({endedAt:new Date().toISOString(),endedReason:o,state:{phase:"ended"}}),g(o),t.abort())}}async function Q(e,t,{label:n,signal:o}){try{const r=await e();await he(r.readable.pipeThrough(Pt()),de(t,{flags:"a"}))}catch(r){if(o?.aborted)return;const s=r instanceof Error?r.message:String(r);g(`${n} ended with an error: ${s}`)}}function Pt(){return new TransformStream({transform:(e,t)=>{t.enqueue(`${JSON.stringify(e)}
3
- `)}})}async function It(e,t,{signal:n}={}){await Q(async()=>await e.debug.createLogStream({signal:n}),t,{label:"log capture",signal:n})}async function Ut(e,t,{signal:n}={}){await Q(async()=>await e.debug.createProxyInterceptStream({signal:n}),t,{label:"network capture",signal:n})}const P=30;async function xt(e,t){await e.pipeTo(zt(t))}function Ct(e){return{decoderConfig:{codec:"avc1.640028",codedHeight:e.height,codedWidth:e.width}}}function zt(e){let t;const n=async()=>{await t?.output.finalize()};return new WritableStream({abort:n,close:n,write:async o=>{o.data.byteLength!==0&&(t??=await _t(Ot(o),e),Dt(t,o),await Wt(t,o))}})}async function _t(e,t){const n=new ge("avc"),o=new we({format:new ye({fastStart:"fragmented"}),target:new me(t,{chunked:!1})});return o.addVideoTrack(n,{frameRate:P}),await o.start(),{frame:0,output:o,seen:{height:e.height,width:e.width},size:{height:e.height,width:e.width},source:n}}function Dt(e,t){if(t.type!=="key")return;const{seen:n,size:o}=e;t.width===n.width&&t.height===n.height||(e.seen={height:t.height,width:t.width},g(`video resized to ${String(t.width)}x${String(t.height)}; keeping ${String(o.width)}x${String(o.height)}`))}function Ot(e){if(e.type!=="key")throw new Error("Video stream started without a key chunk");return e}async function Wt(e,t){const n=new ve(t.data,t.type,e.frame/P,1/P);e.frame+=1,await e.source.add(n,Ct(e.size))}function Mt(e,t,{signal:n}={}){let o,r;const s=async(c,{force:u=!1}={})=>{if(le(c)){if(!u)throw new Error(`A recording already exists at ${c}; pass --force to overwrite`);ue(c)}const h=await e.video.createStream({signal:n});return o={done:qt(h,c),path:c,stream:h},t.save({video:{path:c,recording:!0}}),g(`recording video to ${c}`),{path:c}},i=async c=>{c.stream.close();try{await c.done,t.save({video:{path:c.path,recording:!1}})}finally{o=void 0,r=void 0}},d=async()=>{if(!o)return;const{path:c}=o;await i(o),g(`closed the video recording: ${c}`)};return n?.addEventListener("abort",()=>{d()},{once:!0}),{start:async({force:c=!1,path:u})=>{const h=o?.path??r;if(h!==void 0)throw new Error(`Already recording to ${h}`);r=u;try{return await s(u,{force:c})}finally{r=void 0}},stop:async()=>{if(!o)throw new Error("No video recording in progress");const{path:c}=o;return await i(o),{path:c}}}}async function qt(e,t){try{await xt(e.readable,t)}catch(n){g(`video capture ended with an error: ${String(n)}`)}}async function jt(){const{config:e,logs:t,store:n}=wt();g(`recording to ${n.path}`);const{shutdown:o,signal:r}=Rt(n);try{const{device:s,transport:i}=await gt(e,n,{onClose:o,signal:r}),d=Mt(s,n,{signal:r});await mt(s,e,n,{signal:r}),await bt(i,n,{signal:r});const c=await vt(i,n,e.sessionId,{recorder:d,signal:r});g(`control socket listening at ${c}`),It(s,t.device,{signal:r}),g(`recording device logs to ${t.device}`),t.network&&(Ut(s,t.network,{signal:r}),g(`recording network events to ${t.network}`))}catch(s){if(r.aborted)return;const i=s instanceof Error?s.message:String(s);n.save({endedAt:new Date().toISOString(),endedReason:i,state:{message:i,phase:"error"}}),g(`error: ${i}`),process.exitCode=1}}jt();
1
+ import{T as _,P as A,Q as F,R as E,V as z,J as V,W as j,y as v,X as N,x as H,Y as Q,Z as J,_ as $,$ as G,a0 as K,F as Z,a1 as X,a2 as Y,a3 as B,a4 as ee}from"./stream-C9RfmvVk.js";import T from"debug";import{createServer as R}from"node:net";import{pipeline as P}from"node:stream";import{rm as te}from"node:fs/promises";import{readFileSync as ne,createWriteStream as oe,existsSync as re,rmSync as se}from"node:fs";import{createServer as ie}from"node:http";import{WebSocketServer as ae}from"ws";import{pipeline as ce}from"node:stream/promises";import{EncodedVideoPacketSource as de,Output as ue,FilePathTarget as le,Mp4OutputFormat as fe,EncodedPacket as pe}from"mediabunny";import"json-rpc-2.0";import"zod/mini";import"node:path";import"node:os";import"stream";import"module";const he=T("appetize.io:transport:ws-ping"),we=3e4;function ge(e){let t=!0;const n=()=>{t=!0};e.on("pong",n);const o=setInterval(()=>{if(!t){clearInterval(o),he("liveness ping went unanswered, terminating"),e.terminate();return}e.readyState===e.OPEN&&(t=!1,e.ping())},we);return{destroy(){clearInterval(o),e.off("pong",n)},pong:n}}const w=T("appetize.io:trace:transport:websocket");class L extends _{constructor(t,{signal:n}={}){super(async r=>{this.#e.send(A(r))},{signal:n}),this.wsOrUrl=t,typeof t=="string"?(this.#e=new WebSocket(t),this.#e.binaryType="arraybuffer"):this.#e=t;const o=this.#e;if(o instanceof WebSocket?(o.addEventListener("open",this.#s),o.addEventListener("message",this.#r),o.addEventListener("error",this.#o),o.addEventListener("close",this.#n),this.#t.push(()=>{o.removeEventListener("open",this.#s),o.removeEventListener("message",this.#r),o.removeEventListener("error",this.#o),o.removeEventListener("close",this.#n)})):(o.addEventListener("open",this.#s),o.addEventListener("message",this.#r),o.addEventListener("error",this.#o),o.addEventListener("close",this.#n),this.#t.push(()=>{o.removeEventListener("open",this.#s),o.removeEventListener("message",this.#r),o.removeEventListener("error",this.#o),o.removeEventListener("close",this.#n)})),this.send=o.send.bind(o),"ping"in o){const{destroy:r,pong:s}=ge(o);this.#c=s,this.#t.push(r)}}wsOrUrl;onclose;onerror;onmessage;onopen;send;get bufferedAmount(){return this.#e.bufferedAmount}get readyState(){return this.#e.readyState}#t=[];#i=!1;#a=!1;#c;#e;close({destroyWs:t=!1,...n}={}){this.closed||(this.#i=t,super.close(n))}handleClose(t){w("close()");for(const n of this.#t.splice(0))n();if((typeof this.wsOrUrl=="string"||this.#i)&&(this.#e.close(t.code,t.reason),"terminate"in this.#e)){const n=this.#e;setTimeout(()=>{n.terminate()},100)}this.#d(),super.handleClose(t)}#d(t){this.#a||(this.#a=!0,this.onclose?.(t))}#n=t=>{w("handleWebSocketClose()",t);try{this.#d(t)}finally{this.close()}};#o=t=>{w("handleWebSocketError()",t),this.onerror?.(t)};#r=async t=>{w("handleWebSocketMessage()",t),this.#c?.();const n=this.#u(t);if(!n){w("handleWebSocketMessage() -> legacy"),this.onmessage?.(t);return}w("handleWebSocketMessage() -> jsonrpc",n),this[F].dispatchMessage(n)};#s=t=>{w("handleWebSocketOpen()",t),this.onopen?.(t)};#u(t){try{const n=E(new Uint8Array(t.data));return z(n)?n:null}catch{return null}}}function b(e){return typeof e=="object"&&e!==null}function I(e){try{const t=E(new Uint8Array(e.data));if(b(t)&&typeof t.name=="string")return{name:t.name,payload:"payload"in t?t.payload:void 0}}catch{}}async function ve(e,t={}){const n=t.baseUrl??(typeof e=="string"?new URL(e).origin:void 0),o=typeof e=="string"?new L(e,{signal:t.signal}):e,r=new V(o,{...t,baseUrl:n});return typeof e=="string"&&await me(o,r,t),r}function me(e,t,n){const o=j(n)?.onLegacyMessage;return new Promise((r,s)=>{e.onopen=()=>{r()},e.onclose=()=>{t.close(),n.signal?.aborted?s(n.signal.reason):s(new Error("The websocket was closed before the device connection was established"))},e.onerror=s,e.onmessage=i=>{const c=I(i);c&&o?.(c)}})}async function ye(e,{onStateChange:t,signal:n}){n?.throwIfAborted(),t?.({phase:"starting"}),await new Promise((o,r)=>{const s=()=>{i(),r(n?.reason)},i=()=>{e.onmessage=void 0,e.onclose=void 0,n?.removeEventListener("abort",s)};e.onmessage=c=>{const a=be(I(c));a&&(t?.(a),a.phase==="ready"&&(i(),o()))},e.onclose=()=>{i(),r(new Error("The device closed before it was ready"))},n?.addEventListener("abort",s,{once:!0})})}function be(e){if(!e)return;if(e.name==="ready")return{phase:"ready"};if(e.name!=="launchStatus")return;switch(b(e.payload)&&typeof e.payload.status=="string"?e.payload.status:void 0){case"downloadingApp":return{phase:"downloadingApp"};case"installingApp":return{phase:"installingApp"};case"launchingApp":return{phase:"launchingApp"};default:return}}function Se(){return typeof process>"u"?void 0:process}function Ee(e){return e==="intercept"?"interceptOnDemand":e}function Te(e,{onClose:t,onEvent:n,signal:o}={}){return new Promise((r,s)=>{const i=new WebSocket(e);i.binaryType="arraybuffer",i.onopen=()=>{W(i,{capabilities:{},t:"handshake"})},i.onmessage=c=>{Le(i,c,{onEvent:n,onHandshake:a=>{r({close:()=>{i.close()},id:a})}})},i.onclose=()=>{s(new v("disconnected",`The connection to ${e} closed`)),t?.()},o?.addEventListener("abort",()=>{s(o.reason),i.close()},{once:!0})})}function Le(e,t,n){const o=E(new Uint8Array(t.data));b(o)&&(o.t==="handshake"&&typeof o.id=="string"?n.onHandshake(o.id):o.name==="ping"?W(e,{name:"pong"}):typeof o.name=="string"&&n.onEvent?.(o.name,o.payload))}function W(e,t){e.send(A(t))}async function ke(e,t,{apiToken:n,onQueue:o,signal:r}={}){const s=N();s.promise.catch(()=>{}),Pe(s.reject,r);const i=await Te(Ie(e),{onClose:()=>{s.reject(Ae())},onEvent:(c,a)=>{$e(c,a,s,o)},signal:r});return Re(e,{apiToken:n,data:t,socketid:i.id},r).catch(s.reject),await s.promise.finally(()=>{i.close()})}function Ae(){return new v("disconnected","The webserver connection closed before a session was granted")}function U(e,t){return b(e)?e[t]:void 0}function $e(e,t,n,o){if(e==="queue"||e==="concurrentQueue"){const r=e==="queue"?Ue(t):Ce(t);r&&o?.(r)}else e==="newSession"?We(n,t):e==="userError"&&n.reject(new v("denied",De(t)))}function C(e,t){const n=U(e,t);return typeof n=="number"?n:void 0}function m(e,t){const n=U(e,t);return typeof n=="string"?n:void 0}async function Re(e,t,n){const o=await fetch(new URL("/api/session/request",e),{body:JSON.stringify(t),headers:{"content-type":"application/json"},method:"POST",signal:n});if(!o.ok)throw new v("denied",await qe(o))}function Pe(e,t){t?.addEventListener("abort",()=>{e(t.reason)},{once:!0})}function Ie(e){const t=new URL("/client",e);return t.protocol=t.protocol==="http:"?"ws:":"wss:",t.href}function We(e,t){const n=m(t,"path"),o=m(t,"sessionToken");n!==void 0&&o!==void 0?e.resolve({path:n,sessionToken:o}):e.reject(new v("denied","The granted session is missing its address"))}function Ue(e){const t=C(e,"position");return t===void 0?void 0:{position:t,reason:"capacity"}}function Ce(e){const t=C(e,"position");if(t!==void 0)return{limit:m(e,"name")??"Queue",position:t,reason:"concurrency"}}function De(e){return m(e,"message")??"The session request was denied"}async function qe(e){const t=`The webserver rejected the session request (${String(e.status)})`,n=await e.json().catch(()=>{}),o=m(n,"message");return o?`${t}: ${o}`:t}async function Me({apiToken:e=Oe(),codec:t="h264",device:n,endpoint:o="https://appetize.io",launch:r,onStateChange:s,proxy:i,signal:c,target:a}){c?.throwIfAborted(),H({apiToken:e,codec:t,device:n,endpoint:o,launch:r,proxy:i,target:a}),s?.({phase:"requesting"});const d=await ke(o,{codec:t,noVideo:!0,proxy:Ee(i),publicKey:a,...J(n),...Q(r)},{apiToken:e,onQueue:h=>{s?.({phase:"queued",queue:h})},signal:c});return{baseUrl:new URL(d.path).origin,sessionToken:d.sessionToken,url:xe(d)}}function Oe(){return Se()?.env.APPETIZE_API_TOKEN}function xe(e){const t=new URL("/client",e.path);return t.protocol=t.protocol==="http:"?"ws:":"wss:",t.searchParams.set("sessionToken",e.sessionToken),t.href}async function _e(e,{host:t="127.0.0.1",onError:n,port:o=0,signal:r}={}){r?.throwIfAborted();let s;const i=()=>{const u=s;s=void 0,u&&(u.tunnel?.close(),u.socket.destroy())},c=async u=>{const{socket:l}=u;try{const p=await e.debug.createAdbTunnel();if(l.destroyed){p.close();return}u.tunnel=p,p.closed.catch(S=>{n?.(S)}),Fe(l,p)}catch(p){l.destroyed||n?.(p),l.destroy()}},a=R(u=>{i();const l={socket:u};s=l,u.once("close",()=>{s===l&&(s=void 0)}),c(l)}),d=()=>{r?.removeEventListener("abort",d),i(),a.close()};r?.addEventListener("abort",d,{once:!0});const h=await ze(a,t,o);return{close:d,url:new URL(`adb://${h.address}:${String(h.port)}`)}}function Fe(e,t){P(t.readable,e,D),P(e,async n=>{const o=t.writable.getWriter();try{for await(const r of n)await o.write(r);await o.close()}catch(r){throw await o.abort(r),r}},D)}function D(){}function ze(e,t,n){return new Promise((o,r)=>{e.once("error",r),e.listen({host:t,port:n},()=>{e.off("error",r);const s=e.address();if(s===null||typeof s=="string"){r(new Error("The listener has no tcp address"));return}o(s)})})}async function Ve(e,{methods:t={},path:n,signal:o}){o?.throwIfAborted(),await te(n,{force:!0});const r=new Set,s=R(c=>{r.add(c),c.once("close",()=>{r.delete(c)}),$(new G(c),e,{leftEffects:()=>t})}),i=()=>{o?.removeEventListener("abort",i);for(const c of r)c.destroy();s.close()};return await je(s,n),o?.aborted&&(i(),o.throwIfAborted()),o?.addEventListener("abort",i,{once:!0}),{close:i,path:n}}function je(e,t){return new Promise((n,o)=>{e.once("error",o),e.listen(t,()=>{e.off("error",o),n()})})}const Ne=T("appetize:daemon");function f(e){Ne(e)}function He(e,{message:t="The operation timed out"}={}){const n=new AbortController,o=setTimeout(()=>{n.abort(new Error(t))},e),r=()=>{clearTimeout(o)};return{cancel:r,signal:n.signal,[Symbol.dispose]:r}}var Qe=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),q=e=>{throw TypeError(e)},Je=(e,t,n)=>{if(t!=null){typeof t!="object"&&typeof t!="function"&&q("Object expected");var o,r;o===void 0&&(o=t[Qe("dispose")]),typeof o!="function"&&q("Object not disposable"),r&&(o=function(){try{r.call(this)}catch(s){return Promise.reject(s)}}),e.push([n,o,t])}return t},Ge=(e,t,n)=>{var o=typeof SuppressedError=="function"?SuppressedError:function(i,c,a,d){return d=Error(a),d.name="SuppressedError",d.error=i,d.suppressed=c,d},r=i=>t=n?new o(i,t,"An error was suppressed during disposal"):(n=!0,i),s=i=>{for(;i=e.pop();)try{var c=i[1]&&i[1].call(i[2]);if(i[0])return Promise.resolve(c).then(s,a=>(r(a),s()))}catch(a){r(a)}if(n)throw t};return s()};const Ke=1e4;async function Ze(e,t,{onClose:n,signal:o}={}){var r=[];try{const c=Je(r,He(Ke,{message:"Timed out waiting for session request state"})),a=AbortSignal.any([o,c.signal].filter(g=>!!g)),d=g=>{t.save({state:g}),f(g.phase),g.phase!=="requesting"&&c.cancel()},{featureFlags:h,sessionId:u,...l}=e,{baseUrl:p,url:S}=await Me({...l,onStateChange:d,signal:a});t.save({baseUrl:p});const y=new L(S,{signal:a});return await ye(y,{onStateChange:d,signal:a}),y.onclose=()=>{n?.("The device connection closed")},{device:await ve(y,{baseUrl:p,signal:a}),transport:y}}catch(c){var s=c,i=!0}finally{Ge(r,s,i)}}function Xe(){const e=K(),t=Z(e.sessionId,e.proxy),n=X({device:e.device,endpoint:e.endpoint,open:e.launch?.url,pid:process.pid,proxy:e.proxy,sessionId:e.sessionId,startedAt:new Date().toISOString(),target:e.target});return n.save({logs:t}),{config:e,logs:t,store:n}}async function Ye(e,t,n,{signal:o}={}){try{if(!await Be(t))return;const r=await _e(e,{onError:s=>{f(`adb endpoint error: ${M(s)}`)},signal:o});n.save({adbSerial:r.url.host}),f(`adb endpoint listening at ${r.url.host}`)}catch(r){f(`adb endpoint unavailable: ${M(r)}`)}}async function Be(e){return await Y(e.device.id,{endpoint:e.endpoint})==="android"}function M(e){return e instanceof Error?e.message:String(e)}async function et(e,t,n,{recorder:o,signal:r}){const{path:s}=await Ve(e,{methods:{"appetizeio.cli.record.start":async i=>await o.start(i),"appetizeio.cli.record.stop":async()=>await o.stop()},path:B(n),signal:r});return t.save({controlSocket:s}),s}const O="127.0.0.1";async function tt(e,t,n={}){try{const o=await nt(e,n);return t.save({viewerUrl:o.url.href}),f(`web endpoint listening at ${o.url.href}`),o}catch(o){f(`web endpoint unavailable: ${at(o)}`);return}}async function nt(e,{port:t=0,signal:n,viewerHtml:o=ne(new URL("./viewer.html",import.meta.url),"utf8")}){n?.throwIfAborted();const r=ie((u,l)=>{rt(u,l,o)}),s=new Set,i=new ae({noServer:!0,perMessageDeflate:!1});let c;i.on("connection",u=>{if(s.size>0){u.close(1013,"Viewer already connected");return}s.add(u);const l=new L(u);$(l,e),u.once("close",()=>{s.delete(u)})}),r.on("upgrade",(u,l,p)=>{st(i,u,l,p)});const a=()=>(c??=new Promise((u,l)=>{n?.removeEventListener("abort",d);for(const p of s)ot(p);i.close(),r.close(p=>{p?l(p):u()})}),c),d=()=>{a()};await it(r,t),n?.addEventListener("abort",d,{once:!0});const h=r.address();if(!h||typeof h=="string")throw await a(),new Error("Web address unavailable");return{close:a,url:new URL(`http://${O}:${String(h.port)}/`)}}function ot(e){e.close(1001)}function rt(e,t,n){if(e.method==="GET"&&e.url==="/"){t.writeHead(200,{"Cache-Control":"no-store","Content-Type":"text/html; charset=utf-8"}),t.end(n);return}t.writeHead(404).end()}function st(e,t,n,o){if(t.url!=="/ws"){n.destroy();return}e.handleUpgrade(t,n,o,r=>{e.emit("connection",r,t)})}function it(e,t){return new Promise((n,o)=>{const r=s=>{o(s)};e.once("error",r),e.listen(t,O,()=>{e.removeListener("error",r),n()})})}function at(e){return e instanceof Error?e.message:String(e)}function ct(e){const t=new AbortController,n=dt(e,t),o=ee();return o.addEventListener("abort",()=>{const{reason:r}=o;n(r instanceof Error?r.message:String(r))},{once:!0}),{shutdown:n,signal:t.signal}}function dt(e,t){let n=!1;return o=>{n||(n=!0,e.save({endedAt:new Date().toISOString(),endedReason:o,state:{phase:"ended"}}),f(o),t.abort())}}async function x(e,t,{label:n,signal:o}){try{const r=await e();await ce(r.readable.pipeThrough(ut()),oe(t,{flags:"a"}))}catch(r){if(o?.aborted)return;const s=r instanceof Error?r.message:String(r);f(`${n} ended with an error: ${s}`)}}function ut(){return new TransformStream({transform:(e,t)=>{t.enqueue(`${JSON.stringify(e)}
2
+ `)}})}async function lt(e,t,{signal:n}={}){await x(async()=>await e.debug.createLogStream({signal:n}),t,{label:"log capture",signal:n})}async function ft(e,t,{signal:n}={}){await x(async()=>await e.debug.createProxyInterceptStream({signal:n}),t,{label:"network capture",signal:n})}const k=30;async function pt(e,t){await e.pipeTo(wt(t))}function ht(e){return{decoderConfig:{codec:"avc1.640028",codedHeight:e.height,codedWidth:e.width}}}function wt(e){let t;const n=async()=>{await t?.output.finalize()};return new WritableStream({abort:n,close:n,write:async o=>{o.data.byteLength!==0&&(t??=await gt(mt(o),e),vt(t,o),await yt(t,o))}})}async function gt(e,t){const n=new de("avc"),o=new ue({format:new fe({fastStart:"fragmented"}),target:new le(t,{chunked:!1})});return o.addVideoTrack(n,{frameRate:k}),await o.start(),{frame:0,output:o,seen:{height:e.height,width:e.width},size:{height:e.height,width:e.width},source:n}}function vt(e,t){if(t.type!=="key")return;const{seen:n,size:o}=e;t.width===n.width&&t.height===n.height||(e.seen={height:t.height,width:t.width},f(`video resized to ${String(t.width)}x${String(t.height)}; keeping ${String(o.width)}x${String(o.height)}`))}function mt(e){if(e.type!=="key")throw new Error("Video stream started without a key chunk");return e}async function yt(e,t){const n=new pe(t.data,t.type,e.frame/k,1/k);e.frame+=1,await e.source.add(n,ht(e.size))}function bt(e,t,{signal:n}={}){let o,r;const s=async(a,{force:d=!1}={})=>{if(re(a)){if(!d)throw new Error(`A recording already exists at ${a}; pass --force to overwrite`);se(a)}const h=await e.video.createStream({signal:n});return o={done:St(h,a),path:a,stream:h},t.save({video:{path:a,recording:!0}}),f(`recording video to ${a}`),{path:a}},i=async a=>{a.stream.close();try{await a.done,t.save({video:{path:a.path,recording:!1}})}finally{o=void 0,r=void 0}},c=async()=>{if(!o)return;const{path:a}=o;await i(o),f(`closed the video recording: ${a}`)};return n?.addEventListener("abort",()=>{c()},{once:!0}),{start:async({force:a=!1,path:d})=>{const h=o?.path??r;if(h!==void 0)throw new Error(`Already recording to ${h}`);r=d;try{return await s(d,{force:a})}finally{r=void 0}},stop:async()=>{if(!o)throw new Error("No video recording in progress");const{path:a}=o;return await i(o),{path:a}}}}async function St(e,t){try{await pt(e.readable,t)}catch(n){f(`video capture ended with an error: ${String(n)}`)}}async function Et(){const{config:e,logs:t,store:n}=Xe();f(`recording to ${n.path}`);const{shutdown:o,signal:r}=ct(n);try{const{device:s,transport:i}=await Ze(e,n,{onClose:o,signal:r}),c=bt(s,n,{signal:r});await Ye(s,e,n,{signal:r}),await tt(i,n,{signal:r});const a=await et(i,n,e.sessionId,{recorder:c,signal:r});f(`control socket listening at ${a}`),lt(s,t.device,{signal:r}),f(`recording device logs to ${t.device}`),t.network&&(ft(s,t.network,{signal:r}),f(`recording network events to ${t.network}`))}catch(s){if(r.aborted)return;const i=s instanceof Error?s.message:String(s);n.save({endedAt:new Date().toISOString(),endedReason:i,state:{message:i,phase:"error"}}),f(`error: ${i}`),process.exitCode=1}}Et();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appetize/cli",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Run, inspect and control iOS and Android apps on Appetize cloud devices",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://appetize.io",
@@ -10,7 +10,7 @@ description: >-
10
10
  or answering what is currently on screen — even if Appetize is not mentioned
11
11
  by name.
12
12
  metadata:
13
- cli-version: '0.23.0'
13
+ cli-version: '0.25.0'
14
14
  ---
15
15
 
16
16
  # Appetize
@@ -43,6 +43,7 @@ environment.
43
43
  appetize session start iphone15pro --target app_a1b2c3 --json
44
44
  appetize session start iphone15pro --json
45
45
  appetize session start pixel7 --target app_a1b2c3 --device-os-version 14 --proxy --json
46
+ appetize session start iphone15pro --device-orientation landscapeLeft --device-appearance dark --json
46
47
  ```
47
48
 
48
49
  This blocks until the device is ready; a background daemon then holds the
@@ -199,7 +200,7 @@ appetize open myapp://orders/42 --json
199
200
  appetize inspect --select-test-id order-42 --timeout 5000 --json
200
201
  ```
201
202
 
202
- `open`, like `session start --open`, only delivers the URL, so wait for the
203
+ `open`, like `session start --launch-url`, only delivers the URL, so wait for the
203
204
  destination with `inspect --timeout`. A custom scheme needs its app installed.
204
205
 
205
206
  ## Logged-out state
@@ -29,6 +29,19 @@ Session directories live under `~/.appetize/cli/sessions/`, or
29
29
  - `--proxy http://host:port` routes through your own proxy; other schemes are
30
30
  rejected. Interception rewrites TLS, so an app that pins certificates may
31
31
  fail under it.
32
+ - `--device-orientation`, `--device-locale`, `--device-timezone`,
33
+ `--device-location <lat>,<lng>` (`--device-location=-43.5,172.6` when the
34
+ latitude is negative),
35
+ `--device-appearance` and `--device-font-size` set the device up. iOS takes
36
+ `--device-ios-keyboards` (repeatable) and `--no-device-ios-autocorrect`;
37
+ Android takes `--no-device-android-soft-keyboard` and
38
+ `--device-android-shell`, which needs a target.
39
+ - `--launch-url` opens a URL once ready. These need a target: `--launch-app`
40
+ (another app id), `--no-launch-app` (skip launching), `--launch-permissions`
41
+ (`grant`), `--launch-params` and, on iOS, `--launch-ios-args`. The last two
42
+ repeat; `--launch-params` takes `key=value`, the value JSON when valid
43
+ (`retries=3` is a number, `'zip="90210"'` a string, `'tags=["qa"]'` an array).
44
+ `=` joins a value starting with `-`.
32
45
 
33
46
  ## session stop
34
47