@appetize/cli 0.27.0 → 0.29.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,27 @@
1
1
  # @appetize/cli
2
2
 
3
+ ## 0.29.0
4
+
5
+ ### Minor Changes
6
+
7
+ - feat: app launch, restart, terminate, reinstall and clear commands
8
+ - feat: appetize fold is generally available
9
+ - feat: `media add` adds a photo or video to the device's gallery
10
+
11
+ ### Patch Changes
12
+
13
+ - docs(skill): enroll biometry before authenticating on iOS
14
+
15
+ ## 0.28.0
16
+
17
+ ### Minor Changes
18
+
19
+ - feat(screenshot): `--select-*` captures only the matched element
20
+
21
+ ### Patch Changes
22
+
23
+ - docs: describe element and position selectors in `--help`
24
+
3
25
  ## 0.27.0
4
26
 
5
27
  ### Minor Changes
package/cli.js CHANGED
@@ -1,12 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import Qe from"sade";import{p as U,a as f,b as h,f as j,U as a,d as F,c as et,i as tt,e as nt,g as ot,r as re,h as I,j as $,k as it,D as st,s as rt,l as at,m as ae,n as ce,S as C,o as M,q as B,t as b,u as T,v as D,w as pe,x as ct,y as pt,z as le,A as lt,B as de,C as dt,E as ut,F as ft,G as mt,H as ue,I as fe,J as ht,K as gt,L as wt,M as yt,N as vt,O as bt}from"./stream-Cbdd68uZ.js";import{existsSync as me,openAsBlob as xt,openSync as St,closeSync as Et}from"node:fs";import{basename as q,extname as Ot,resolve as he,dirname as At,join as x,relative as Pt}from"node:path";import{writeFile as ge,stat as kt,rm as It,cp as $t}from"node:fs/promises";import{spawn as Tt}from"node:child_process";import{fileURLToPath as we}from"node:url";import{humanId as Dt}from"human-id";import{homedir as Lt}from"node:os";import Nt from"debug";import _t from"yocto-spinner";import zt from"cli-table3";import"json-rpc-2.0";import"zod/mini";import"stream";import"module";import"node:net";const G=["closed","fullyOpen","partiallyOpen"],ye=["back","menu","unlock","volumeDown","volumeUp"],ve=["home","lock"],be=[...ve,...ye],K=["down","left","right","up"],Rt=async({device:e,options:t})=>{await e.authenticateBiometry({match:t.match!==!1})},Ut=async({device:e,options:t})=>{const n=U(t.unenroll)??!1;await e.settings.setBiometryEnrolled(!n)},xe=[{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:n,id:o,platform:i,versionName:s}){return{appId:e,buildNumber:t,created:n,id:o,platform:i,versionName:s}}const jt=["app"],Ft=10,Ct=[{fields:xe,key:"builds"},{header:"Total",key:"total"},{header:"Next page",key:"nextPage"}];async function Mt({options:e,signal:t}){const n=f(e.app,"--app"),o=h(e.limit,"--limit")??Ft,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 j("/api/v2/builds",{commandOptions:e,query:s,signal:t});return{builds:r.builds.map(Z),nextPage:r.nextPage,total:r.total}}const Bt=["note","tags"],Se=12e4,qt=2e3,V=[".apk",".apks",".zip",".tar.gz",".tgz"],Gt={".apk":"android",".apks":"android",".tar.gz":"ios",".tgz":"ios",".zip":"ios"},Kt=xe;async function Zt({options:e,positionals:[t],progress:n,signal:o}){const i=Xt(t),s=q(t);n.status(`Uploading ${s}`);const r=await Ht(t,i,e,o);if(e.wait!==!0)return n.status(`Uploaded ${s}`),Z(r);n.step(`Uploaded ${s}`),n.status("Waiting for build metadata");const p=await Vt(r.id,e,o);return n.status(`Processed ${s}`),Z(p)}async function Vt(e,t,n){const o=h(t.timeout,"--timeout")??Se,i=Date.now()+o;for(;;){const s=await j(`/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 F(qt,n)}}async function Wt(e){if(!me(e))throw new Error(`No build file at ${e}`);return await xt(e)}function Xt(e){const t=e.toLowerCase(),n=V.find(o=>t.endsWith(o));if(n===void 0)throw new a(`Cannot tell the platform of ${q(e)}; upload a ${V.join(", ")} file`);return Gt[n]}async function Ht(e,t,n,o){const i=new FormData;i.set("platform",t);const s=f(n.note,"--note");s!==void 0&&i.set("note",s);const r=f(n.tags,"--tags");return r!==void 0&&i.set("tags",r),i.set("file",await Wt(e),q(e)),await j("/api/v2/builds",{body:i,commandOptions:n,method:"POST",signal:o})}const Jt=["platform"],Yt=[{fields:[{header:"ID",key:"id"},{header:"Name",key:"name"},{header:"Platform",key:"platform"},{header:"OS versions",key:"osVersions"}],key:"devices"}];async function Qt({options:e,signal:t}){const n=en(e.platform);return{devices:(await et(e,t)).filter(i=>n===void 0||i.platform===n).map(tn)}}function en(e){const t=f(e,"--platform");if(t!==void 0){if(!tt(t))throw new a(`--platform expects one of: ${nt.join(", ")}`);return t}}function tn({id:e,name:t,osVersions:n,platform:o}){return{id:e,name:t,osVersions:n,platform:o}}const nn="cli-fold",on=async({device:e,positionals:[t]})=>{if(!sn(t))throw new a(`Hinge must be one of: ${G.join(", ")}`);await e.fold(t)};function sn(e){return G.some(t=>t===e)}const rn=async({device:e,options:t,positionals:[n]})=>{await e.gesture(cn(n),an(t))};function an(e){const t=h(e.timeout,"--timeout");return t===void 0?void 0:{timeout:t}}function cn(e){let t;try{t=ot(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 pn="APPETIZE_OUTPUT";function ln(e,t,n){return{format:dn(e,t),progress:un(e,n)}}function W(e,{extension:t,strict:n=!1}){const o=Ot(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 dn(e,t){if(e.json===!0)return"json";if(e.json===!1)return"text";const n=process.env[pn]?.trim();return n==="json"||n==="text"?n:t.isTTY===!0?"text":"json"}function un(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",O={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"}},X="index",L="position",H="x",J="y";function Ee(e,{prefix:t=g}={}){return S.reduce((n,o)=>{const{help:i,suffix:s}=O[o];return n.option(`--${l(t,s)}`,i)},e).option(`--${l(t,X)}`,"Which match when multiple (0-based)")}function Y(e,t={}){const{prefix:n=g}=t;return Ee(e,t).option(`--${l(n,L)}`,"Screen position to use as x,y (0-1), e.g., 0.5,0.75").option(`--${l(n,H)}`,"Horizontal position within the element (0-1, outside allowed)").option(`--${l(n,J)}`,"Vertical position within the element (0-1, outside allowed)")}function Oe(e,{prefix:t=g}={}){const n=ee(e,t);if(n.length===0)throw new a(`A selector is required: pass one of ${_(S,t)}`);if(n.length>1)throw new a(`Only one selector may be given, got ${_(n,t)}`);const[o]=n,i=Te(e,l(t,O[o].suffix)),s=h(e[l(t,X)],`--${l(t,X)}`);switch(o){case"testId":return{by:"testId",index:s,testId:i};case"text":return{by:"text",index:s,text:hn(i)}}}function Ae(e){const t=h(e.timeout,"--timeout");return t===void 0?void 0:{timeout:t}}function fn(e,{prefix:t=g}={}){return Ie(e,t)?Oe(e,{prefix:t}):void 0}function N(e,{prefix:t=g}={}){return Ie(e,t)?Pe(e,{prefix:t}):void 0}function Pe(e,{prefix:t=g}={}){const n=l(t,L),o=e[n],i=$e(e,l(t,H)),s=$e(e,l(t,J));if(o===void 0){if(ee(e,t).length===0)throw new a(`A selector is required: pass one of ${A({prefix:t})}`);return{...Oe(e,{prefix:t}),x:i,y:s}}if(ee(e,t).length>0)throw new a(`--${n} may not be combined with ${_(S,t)}`);if(i!==void 0||s!==void 0)throw new a(`--${n} may not be combined with --${l(t,H)} or --${l(t,J)}`);return mn(Te(e,n),n)}function A(e={}){const{prefix:t=g}=e;return`${_(S,t)}, --${l(t,L)}`}function Q(e={}){const{prefix:t=g}=e;return[...ke(e),l(t,L)]}function ke({prefix:e=g}={}){return S.map(t=>l(e,O[t].suffix))}function ee(e,t){return S.filter(n=>e[l(t,O[n].suffix)]!==void 0)}function l(...e){return e.filter(Boolean).join("-")}function _(e,t){return e.map(n=>`--${l(t,O[n].suffix)}`).join(", ")}function Ie(e,t){return Object.entries(e).some(([n,o])=>o!==void 0&&n.startsWith(`${t}-`))}function $e(e,t){return h(e[t],`--${t}`)}function mn(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:re(o,`--${t}`),y:re(i,`--${t}`)}}function hn(e){const t=/^\/(.*)\/([a-z]*)$/s.exec(e);return t?new RegExp(t[1],t[2]):e}function Te(e,t){return I(e[t],`--${t}`)}const gn=ke(),wn=async({device:e,options:t,positionals:[n]})=>{const o=yn(n),i=fn(t),s=await e.inspect(i,Ae(t)),r=vn(s,t.pretty===!0);await ge(o,r);const p=Buffer.byteLength(r),c=De(s.roots);return i?{bytes:p,matches:s.roots.length,nodes:c,output:o}:{bytes:p,nodes:c,output:o}};function De(e){return e.reduce((t,n)=>t+1+De(n.children),0)}function yn(e="inspect"){return W(e,{extension:".json"})}function vn(e,t){return`${JSON.stringify(e,null,t?2:void 0)}
3
- `}const bn=async({device:e})=>{await e.settings.clearKeychain()},xn=async({device:e,positionals:[t]})=>{await e.open(Sn(t))};function Sn(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 En=new Set(be),On=async({device:e,positionals:[t]})=>{if(!An(t))throw new a(`Button must be one of: ${be.join(", ")}`);await e.press(t)};function An(e){return typeof e=="string"&&En.has(e)}const Pn=async({control:e,options:t,positionals:[n]})=>{const{path:o}=await e.recordStart({force:t.force===!0,path:In(n)});return{path:o}},kn=async({control:e})=>{const{path:t}=await e.recordStop();return{path:t}};function In(e){const t=he(W(e,{extension:".mp4",strict:!0})),n=At(t);if(!me(n))throw new a(`No such directory: ${n}`);return t}const $n=async({device:e,positionals:[t]})=>{if(!Tn(t))throw new a(`Orientation must be one of: ${$.join(", ")}`);await e.rotate(t)};function Tn(e){return $.some(t=>t===e)}const Dn=async({device:e,positionals:[t]})=>{const n=await e.screenshot(),o=Nn(n.mimeType,t);return await ge(o,n.data),{bytes:n.data.byteLength,output:o}};function Ln(e){const t=e.split("/").at(1);return`.${t==="jpeg"?"jpg":t??"png"}`}function Nn(e,t="screenshot"){return W(t,{extension:Ln(e)})}function _n(e){const t=we(new URL("./daemon.js",import.meta.url)),n=St(it(e.sessionId),"a"),o=Tt(process.execPath,[t],{detached:!0,env:{...process.env,[st]:rt(e),DEBUG:zn(process.env.DEBUG)},stdio:["ignore",n,n]});if(Et(n),o.unref(),o.pid===void 0)throw new Error("Failed to spawn the session daemon");return o.pid}function zn(e){return e?`${e},appetize:daemon`:"appetize:daemon"}const Rn=/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/i;function Un(){return Dt({capitalize:!1,separator:"-"})}function jn(e){if(!Rn.test(e))throw new a('--session-id must use letters, digits, ".", "-" or "_"');return e}const Le=1e4;function Ne(e){if(!at(e))return!1;try{process.kill(e.pid,"SIGTERM")}catch{}return!0}async function _e(e,t){(Ne(e)||Cn(e))&&await Fn(e,t)}async function Fn({pid:e,sessionId:t},n){const o=ce(e);try{await F(Le,AbortSignal.any([n,o]))}catch(i){if(o.aborted||!ae(e))return;throw i}throw new C(`Session ${t} did not stop in time`,{sessionId:t})}function Cn({endedAt:e,pid:t}){return e!==void 0&&Date.now()-Date.parse(e)<Le&&ae(t)}const Mn=["device-android-shell","device-appearance","device-font-size","device-ios-keyboards","device-locale","device-location","device-orientation","device-os-version","device-timezone"];function Bn(e,t){const n=M({shell:f(e["device-android-shell"],"--device-android-shell"),softKeyboard:U(e["device-android-soft-keyboard"],{defaultValue:!0})}),o=M({autocorrect:U(e["device-ios-autocorrect"],{defaultValue:!0}),keyboards:B(e["device-ios-keyboards"],"--device-ios-keyboards")});return{android:n,appearance:b(e["device-appearance"],D,"--device-appearance"),fontSize:b(e["device-font-size"],T,"--device-font-size"),id:t,ios:o,locale:f(e["device-locale"],"--device-locale"),location:ze(e["device-location"],"--device-location"),orientation:b(e["device-orientation"],$,"--device-orientation"),osVersion:f(e["device-os-version"],"--device-os-version"),timezone:f(e["device-timezone"],"--device-timezone")}}function ze(e,t){const n=f(e,t);if(n===void 0)return;const o=n.split(",").map(r=>r.trim());if(o.length!==2||!o.every(qn))throw new a(`${t} expects <lat>,<lng>, e.g., ${t}=-43.65,79.38`);const[i,s]=o.map(Number);if(Math.abs(i)>90||Math.abs(s)>180)throw new a(`${t} expects latitude within \xB190 and longitude within \xB1180`);return{latitude:i,longitude:s}}function qn(e){return e!==""&&Number.isFinite(Number(e))}const Gn=["launch-app","launch-ios-args","launch-params","launch-permissions","launch-url","open"];function Kn(e){const t=B(e["launch-ios-args"],"--launch-ios-args");return M({app:Zn(e["launch-app"]),ios:t===void 0?void 0:{args:t},params:Vn(e["launch-params"]),permissions:b(e["launch-permissions"],pe,"--launch-permissions"),url:f(e["launch-url"],"--launch-url")??f(e.open,"--open")})}function Zn(e){return e===!1?!1:f(e,"--launch-app")}function Vn(e){const t=B(e,"--launch-params");if(t!==void 0)return Object.fromEntries(t.map(Wn))}function Wn(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),Xn(e.slice(t+1))]}function Xn(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 Hn(e,{endpointFromEnv:t=!1}={}){const n=t?{...Re,endpoint:"APPETIZE_ENDPOINT"}:Re;try{ct(e)}catch(o){throw o instanceof pt?new a(o.issues.map(i=>Jn(i,n)).join(`
4
- `)):o}}function Jn({code:e,message:t,path:n},o){const i=Yn(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 Yn(e,t){return t[e]??`--${e.split(".").filter(n=>!/^\d+$/.test(n)).map(n=>n.replaceAll(/([A-Z])/g,"-$1").toLowerCase()).join("-")}`}const Qn=[...Mn,...Gn,"proxy","target"],eo=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"},no=[{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 oo(e,{onPhase:t,signal:n}){let o;for(;;){const i=de(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 F(eo,n)}}function io(e){if(!(e===void 0||e===!1)){if(e===""||e==="intercept")return"intercept";if(typeof e!="string"||!ro(e))throw new a("--proxy expects 'intercept' or an http:// url, e.g., http://proxy:8080");return e}}async function so({options:e,positionals:t,progress:n,signal:o}){const[i]=t,s=f(e.target,"--target")??co(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=f(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?Un():jn(r),d={device:Bn(e,i),endpoint:lt(e),featureFlags:le(),launch:Kn(e),proxy:io(e.proxy),sessionId:c,target:s};Hn(d,{endpointFromEnv:e.endpoint===void 0});const m=de(c);if(m!==void 0){if(!p)throw new dt(c);n.status(`Replacing session ${c}`),await _e(m,o),ut(c)}_n(d),n.step(`Session ${c}`);const v=ft(c,d.proxy);if(e.wait===!1)return{logs:v,sessionId:c};const P=await oo(c,{onPhase:k=>{ao(k)&&n.status(to[k]??"Starting the session")},signal:o}).catch(k=>{throw C.wrap(k,c)});return{adbSerial:P.adbSerial,baseUrl:P.baseUrl,controlSocket:P.controlSocket,logs:v,sessionId:c,viewerUrl:P.viewerUrl}}function ro(e){return e.startsWith("http://")}function ao(e){return e!=="ended"&&e!=="error"}function co(e){const t=e._;return Array.isArray(t)&&typeof t[0]=="string"?t[0]:void 0}const po=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}),lo=["appearance","font-size","location"],uo=async({device:e,options:t})=>{const n=b(t.appearance,D,"--appearance"),o=b(t["font-size"],T,"--font-size"),i=ze(t.location,"--location");if(!n&&!o&&!i)throw new a("Pass at least one of --appearance, --font-size or --location");n&&await e.settings.setAppearance(n),o&&await e.settings.setFontSize(o),i&&await e.settings.setLocation(i)},fo=async({device:e})=>{await e.shake()},Ue="appetize",mo=we(new URL(`./skills/${Ue}/`,import.meta.url)),te=["agents","claude","codex","copilot","cursor"],je=["project","user"],ho={agents:".agents",claude:".claude",codex:".codex",copilot:".copilot",cursor:".cursor"},go=x(".agents","skills"),wo=["claude","agents"];function yo(e){return te.some(t=>t===e)}function vo(e){return je.some(t=>t===e)}async function bo(e){const t=e.agents??await xo(e),n=await Promise.all(t.map(o=>Fe(o,e)));return[...new Set(n.flat())]}async function Fe(e,t){const n=Ce(e,t);if(e==="claude")return[x(n,"skills")];const o=x(t.root,go);return t.scope==="project"?[o]:await Me(n)?[o,x(n,"skills")]:[o]}function Ce(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,ho[e])}async function xo(e){const n=(await Promise.all(te.map(async o=>await So(o,e)?o:void 0))).filter(o=>o!==void 0);return n.length>0?n:wo}async function So(e,t){const n=[Ce(e,t),...await Fe(e,t)];return(await Promise.all(n.map(i=>Me(i)))).includes(!0)}async function Me(e){try{return(await kt(e)).isDirectory()}catch{return!1}}const Eo=["agent","scope"];async function Oo({options:e,signal:t}){const n=ko(e.scope),o=Po(e.agent),i=n==="user"?Lt():process.cwd(),s=await bo({agents:o,root:i,scope:n}),r=[];for(const p of s){t.throwIfAborted();const c=x(p,Ue);await It(c,{force:!0,recursive:!0}),await $t(mo,c,{recursive:!0}),r.push(n==="user"?c:Pt(i,c))}return{installed:r}}function Ao(e){const t=I(e,"--agent");if(!yo(t))throw new a(`--agent expects one of ${te.join(", ")}`);return t}function Po(e){return e===void 0?void 0:(Array.isArray(e)?e:[e]).map(n=>Ao(n))}function ko(e){if(e===void 0)return"project";const t=I(e,"--scope");if(!vo(t))throw new a(`--scope expects one of ${je.join(", ")}`);return t}const w="from",y="to",Io=[...Q({prefix:w}),...Q({prefix:y})],$o=async({device:e,options:t})=>{await e.swipe(No(t),zo(t))};function To(e){return K.some(t=>t===e)}function Do(e){if(e===void 0)return;const t=I(e,"--direction");if(!To(t))throw new a(`--direction expects one of ${K.join(", ")}`);return t}function Lo(e,t){const n=mt(e.distance,"--distance");if(N(e,{prefix:y})!==void 0)throw new a(`--direction may not be combined with --${y}-*`);return{direction:t,distance:n,from:N(e,{prefix:w})}}function No(e){const t=Do(e.direction);return t===void 0?_o(e):Lo(e,t)}function _o(e){if(e.distance!==void 0)throw new a("--distance is only valid with --direction");return{from:Be(N(e,{prefix:w}),w),to:Be(N(e,{prefix:y}),y)}}function zo(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 Be(e,t){if(e===void 0)throw new a(`A swipe needs --direction, or both endpoints: pass one of ${A({prefix:t})}`);return e}const Ro=Q(),Uo=async({device:e,options:t})=>{const n=Pe(t);await e.tap(n,Ae(t))},jo=async({device:e,options:t,positionals:[n]})=>{await e.type(n,Fo(t))};function Fo(e){const t=h(e.delay,"--delay");return t===void 0?void 0:{delay:t}}const ne=Nt("appetize:cli"),Co={fail:()=>{},status:()=>{},step:()=>{},succeed:()=>{}};function Mo(e,t,n){return e==="interactive"?Bo(t,n):Co}function Bo(e,t){const n=_t({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 qo=new Set(["adb","api","apk","id","os","url"]),Go={bottom:"","bottom-left":"","bottom-mid":"","bottom-right":"",left:"","left-mid":"",mid:"","mid-mid":"",middle:" ",right:"","right-mid":"",top:"","top-left":"","top-mid":"","top-right":""},qe=2,Ko=10;function Zo(e){return e.split(/(?=[A-Z])/).map((t,n)=>{const o=t.toLowerCase();return qo.has(o)?o.toUpperCase():n===0?o.charAt(0).toUpperCase()+o.slice(1):o}).join(" ")}function Vo(e,t,n){return ie(e)?Xe(e,t,n):z(e,{fields:t,key:""},n)}function Wo(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 Ge(e){return new zt({chars:Go,head:e,style:{border:[],head:[],"padding-left":0,"padding-right":qe}})}function Ke(e,t){return e.find(n=>n.key===t)}function Ze(e,t){return t?.header??Zo(e)}function oe(e){return e!=null}function ie(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Ve(e){return["boolean","number","string"].includes(typeof e)}function We(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 Xe(e,t,n){const o=new Map(Object.entries(e)),i=We(t,[...o.keys()]).filter(m=>oe(o.get(m))),s=i.map(m=>Ke(t,m)),r=i.map((m,v)=>Ze(m,s[v])),p=Math.max(0,...r.map(m=>m.length))+qe,c=n===void 0?void 0:n-p-1,d=Ge();return d.push(...i.map((m,v)=>[r[v],z(o.get(m),s[v],c)])),He(d.toString())}function Xo(e,t){const n=e.map(r=>new Map(Object.entries(r))),o=We(t,n.flatMap(r=>[...r.keys()])),i=o.map(r=>Ke(t,r)),s=Ge(o.map((r,p)=>Ze(r,i[p])));for(const r of n)s.push(o.map((p,c)=>{const d=r.get(p);return oe(d)?z(d,i[c]):""}));return He(s.toString())}function z(e,t,n){if(t?.format)return R(t.format(e),n);if(Ve(e))return R(ue(e),n);if(Array.isArray(e)){const o=e.filter(oe);return o.every(Ve)?R(ue(o),n):o.every(ie)?Xo(o,t?.fields??[]):o.map(i=>z(i,t,n)).join(`
6
- `)}return ie(e)?Xe(e,t?.fields??[],n):R(String(e),n)}function He(e){return e.split(`
2
+ import at from"sade";import{p as I,n as C,a as f,b as x,U as r,l as de,c as M,d as h,f as B,e as q,g as rt,i as ct,h as pt,j as lt,r as ue,k as T,m as L,o as dt,D as ut,s as ft,q as mt,t as fe,u as me,S as G,v as D,w as N,x as ht,y as gt,z as wt,A as yt,B as he,C as vt,E as xt,F as bt,G as St,H as ge,I as we,J as Et,K as Ot,L as At,M as kt,N as Pt,O as $t}from"./stream-BByjqBT3.js";import{existsSync as ye,openAsBlob as ve,openSync as It,closeSync as Tt}from"node:fs";import{basename as _,extname as xe,resolve as be,dirname as Lt,join as b,relative as Dt}from"node:path";import{writeFile as Se,stat as Ee,rm as Nt,cp as _t}from"node:fs/promises";import{spawn as zt}from"node:child_process";import{fileURLToPath as Oe}from"node:url";import{humanId as Rt}from"human-id";import{homedir as jt}from"node:os";import Ut from"debug";import Ft from"yocto-spinner";import Ct from"cli-table3";import"json-rpc-2.0";import"zod/mini";import"stream";import"module";import"node:net";const K=["closed","fullyOpen","partiallyOpen"],Z={"image/gif":["gif"],"image/jpeg":["jpg","jpeg"],"image/png":["png"],"video/mp4":["mp4"]},Ae=["back","menu","unlock","volumeDown","volumeUp"],ke=["home","lock"],Pe=[...ke,...Ae],W=["down","left","right","up"],Mt=["launch-app","launch-ios-args","launch-params","launch-permissions","launch-url","open"];function Bt(e){const t=I(e["launch-ios-args"],"--launch-ios-args");return C({app:qt(e["launch-app"]),ios:t===void 0?void 0:{args:t},params:$e(e["launch-params"],"--launch-params"),permissions:x(e["launch-permissions"],de,"--launch-permissions"),url:f(e["launch-url"],"--launch-url")??f(e.open,"--open")})}function $e(e,t){const n=I(e,t);if(n!==void 0)return Object.fromEntries(n.map(o=>Gt(o,t)))}function qt(e){return e===!1?!1:f(e,"--launch-app")}function Gt(e,t){const n=e.indexOf("=");if(n<1)throw new r(`${t} expects key=value, e.g., ${t} debug=true`);return[e.slice(0,n),Kt(e.slice(n+1))]}function Kt(e){try{return JSON.parse(e)}catch{return e}}const Zt=["app","ios-args","params"],Wt=async({device:e,options:t})=>{await e.app.clearState(O(t))},Vt=async({device:e,options:t})=>{await e.app.launch(O(t),Ie(t))},Ht=async({device:e,options:t})=>{await e.app.reinstall(O(t))},Xt=async({device:e,options:t})=>{await e.app.restart(O(t),Ie(t))},Jt=async({device:e,options:t})=>{await e.app.terminate(O(t))};function Ie(e){const t=I(e["ios-args"],"--ios-args");return{ios:t===void 0?void 0:{args:t},params:$e(e.params,"--params")}}function O(e){return f(e.app,"--app")}const Yt=async({device:e,options:t})=>{await e.authenticateBiometry({match:t.match!==!1})},Qt=async({device:e,options:t})=>{const n=M(t.unenroll)??!1;await e.settings.setBiometryEnrolled(!n)},Te=[{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 V({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 en=["app"],tn=10,nn=[{fields:Te,key:"builds"},{header:"Total",key:"total"},{header:"Next page",key:"nextPage"}];async function on({options:e,signal:t}){const n=f(e.app,"--app"),o=h(e.limit,"--limit")??tn,i=h(e.page,"--page"),s={limit:String(o)};n!==void 0&&(s.appId=n),i!==void 0&&(s.page=String(i));const a=await B("/api/v2/builds",{commandOptions:e,query:s,signal:t});return{builds:a.builds.map(V),nextPage:a.nextPage,total:a.total}}const sn=["note","tags"],Le=12e4,an=2e3,H=[".apk",".apks",".zip",".tar.gz",".tgz"],rn={".apk":"android",".apks":"android",".tar.gz":"ios",".tgz":"ios",".zip":"ios"},cn=Te;async function pn({options:e,positionals:[t],progress:n,signal:o}){const i=un(t),s=_(t);n.status(`Uploading ${s}`);const a=await fn(t,i,e,o);if(e.wait!==!0)return n.status(`Uploaded ${s}`),V(a);n.step(`Uploaded ${s}`),n.status("Waiting for build metadata");const l=await ln(a.id,e,o);return n.status(`Processed ${s}`),V(l)}async function ln(e,t,n){const o=h(t.timeout,"--timeout")??Le,i=Date.now()+o;for(;;){const s=await B(`/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 q(an,n)}}async function dn(e){if(!ye(e))throw new Error(`No build file at ${e}`);return await ve(e)}function un(e){const t=e.toLowerCase(),n=H.find(o=>t.endsWith(o));if(n===void 0)throw new r(`Cannot tell the platform of ${_(e)}; upload a ${H.join(", ")} file`);return rn[n]}async function fn(e,t,n,o){const i=new FormData;i.set("platform",t);const s=f(n.note,"--note");s!==void 0&&i.set("note",s);const a=f(n.tags,"--tags");return a!==void 0&&i.set("tags",a),i.set("file",await dn(e),_(e)),await B("/api/v2/builds",{body:i,commandOptions:n,method:"POST",signal:o})}const mn=["platform"],hn=[{fields:[{header:"ID",key:"id"},{header:"Name",key:"name"},{header:"Platform",key:"platform"},{header:"OS versions",key:"osVersions"}],key:"devices"}];async function gn({options:e,signal:t}){const n=wn(e.platform);return{devices:(await rt(e,t)).filter(i=>n===void 0||i.platform===n).map(yn)}}function wn(e){const t=f(e,"--platform");if(t!==void 0){if(!ct(t))throw new r(`--platform expects one of: ${pt.join(", ")}`);return t}}function yn({id:e,name:t,osVersions:n,platform:o}){return{id:e,name:t,osVersions:n,platform:o}}const vn=async({device:e,positionals:[t]})=>{if(!xn(t))throw new r(`Hinge must be one of: ${K.join(", ")}`);await e.fold(t)};function xn(e){return K.some(t=>t===e)}const bn=async({device:e,options:t,positionals:[n]})=>{await e.gesture(En(n),Sn(t))};function Sn(e){const t=h(e.timeout,"--timeout");return t===void 0?void 0:{timeout:t}}function En(e){let t;try{t=lt(e)}catch(n){throw n instanceof SyntaxError?new r(n.message):n}if(t.length===0)throw new r("Expected at least one gesture step");return t}const On="APPETIZE_OUTPUT";function An(e,t,n){return{format:kn(e,t),progress:Pn(e,n)}}function X(e,{extension:t,strict:n=!1}){const o=xe(e);if(o==="")return`${e}${t}`;if(n&&o!==t)throw new r(`The output must be a path ending in ${t}`);return e}function kn(e,t){if(e.json===!0)return"json";if(e.json===!1)return"text";const n=process.env[On]?.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",A={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"}},$n=` Element selectors
3
+ Commands that act on UI elements (e.g., inspect) can take one or more
4
+ element selectors, as flags ending in one of:
5
+
6
+ -text Exact string or /regex/flags, matched against
7
+ Android: text, content-desc, hintText
8
+ iOS: title (or value when empty), label, placeholderValue
9
+ -test-id Exact id
10
+ Android: resource-id, whole or after its last /
11
+ iOS: accessibilityIdentifier
12
+ -index Which match when multiple (0-based)
13
+
14
+ Position selectors
15
+ Commands that act on a screen position (e.g., tap) take position
16
+ selectors. Every element selector is a position selector, at the
17
+ element's center by default.
18
+
19
+ -position Screen position x,y (0-1)
20
+ -x, -y Position within the element (0-1, outside allowed)
21
+
22
+ `,J="index",z="position",Y="x",Q="y";function ee(e,{prefix:t=g}={}){return S.reduce((n,o)=>{const{help:i,suffix:s}=A[o];return n.option(`--${d(t,s)}`,i)},e).option(`--${d(t,J)}`,"Which match when multiple (0-based)")}function te(e,t={}){const{prefix:n=g}=t;return ee(e,t).option(`--${d(n,z)}`,"Screen position to use as x,y (0-1), e.g., 0.5,0.75").option(`--${d(n,Y)}`,"Horizontal position within the element (0-1, outside allowed)").option(`--${d(n,Q)}`,"Vertical position within the element (0-1, outside allowed)")}function De(e,{prefix:t=g}={}){const n=se(e,t);if(n.length===0)throw new r(`A selector is required: pass one of ${j(S,t)}`);if(n.length>1)throw new r(`Only one selector may be given, got ${j(n,t)}`);const[o]=n,i=je(e,d(t,A[o].suffix)),s=h(e[d(t,J)],`--${d(t,J)}`);switch(o){case"testId":return{by:"testId",index:s,testId:i};case"text":return{by:"text",index:s,text:Tn(i)}}}function ne(e){const t=h(e.timeout,"--timeout");return t===void 0?void 0:{timeout:t}}function Ne(e,{prefix:t=g}={}){return ze(e,t)?De(e,{prefix:t}):void 0}function R(e,{prefix:t=g}={}){return ze(e,t)?_e(e,{prefix:t}):void 0}function _e(e,{prefix:t=g}={}){const n=d(t,z),o=e[n],i=Re(e,d(t,Y)),s=Re(e,d(t,Q));if(o===void 0){if(se(e,t).length===0)throw new r(`A selector is required: pass one of ${k({prefix:t})}`);return{...De(e,{prefix:t}),x:i,y:s}}if(se(e,t).length>0)throw new r(`--${n} may not be combined with ${j(S,t)}`);if(i!==void 0||s!==void 0)throw new r(`--${n} may not be combined with --${d(t,Y)} or --${d(t,Q)}`);return In(je(e,n),n)}function k(e={}){const{prefix:t=g}=e;return`${j(S,t)}, --${d(t,z)}`}function oe(e={}){const{prefix:t=g}=e;return[...ie(e),d(t,z)]}function ie({prefix:e=g}={}){return S.map(t=>d(e,A[t].suffix))}function se(e,t){return S.filter(n=>e[d(t,A[n].suffix)]!==void 0)}function d(...e){return e.filter(Boolean).join("-")}function j(e,t){return e.map(n=>`--${d(t,A[n].suffix)}`).join(", ")}function ze(e,t){return Object.entries(e).some(([n,o])=>o!==void 0&&n.startsWith(`${t}-`))}function Re(e,t){return h(e[t],`--${t}`)}function In(e,t){const n=e.split(",");if(n.length!==2)throw new r(`--${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:ue(o,`--${t}`),y:ue(i,`--${t}`)}}function Tn(e){const t=/^\/(.*)\/([a-z]*)$/s.exec(e);return t?new RegExp(t[1],t[2]):e}function je(e,t){return T(e[t],`--${t}`)}const Ln=ie(),Dn=async({device:e,options:t,positionals:[n]})=>{const o=Nn(n),i=Ne(t),s=await e.inspect(i,ne(t)),a=_n(s,t.pretty===!0);await Se(o,a);const l=Buffer.byteLength(a),c=Ue(s.roots);return i?{bytes:l,matches:s.roots.length,nodes:c,output:o}:{bytes:l,nodes:c,output:o}};function Ue(e){return e.reduce((t,n)=>t+1+Ue(n.children),0)}function Nn(e="inspect"){return X(e,{extension:".json"})}function _n(e,t){return`${JSON.stringify(e,null,t?2:void 0)}
23
+ `}const zn=async({device:e})=>{await e.settings.clearKeychain()},Rn=async({device:e,positionals:[t]})=>{if(typeof t!="string")throw new r("<path> expects a media file, e.g., photo.png");if(!(await Ee(t).catch(()=>{}))?.isFile())throw new r(`<path> is not a file: ${t}`);const o=Un(xe(t).slice(1).toLowerCase()),i=await ve(t,{type:o??""});await e.addMedia(i,{name:_(t)})};function jn(e){return Object.hasOwn(Z,e)}function Un(e){return Object.keys(Z).find(t=>jn(t)&&Z[t].some(n=>n===e))}const Fn=async({device:e,positionals:[t]})=>{await e.open(Cn(t))};function Cn(e){if(typeof e=="string")try{return new URL(e),e}catch{throw new r("<url> expects a URL, e.g., myapp://orders/42")}throw new r("<url> expects a URL, e.g., myapp://orders/42")}const Mn=new Set(Pe),Bn=async({device:e,positionals:[t]})=>{if(!qn(t))throw new r(`Button must be one of: ${Pe.join(", ")}`);await e.press(t)};function qn(e){return typeof e=="string"&&Mn.has(e)}const Gn=async({control:e,options:t,positionals:[n]})=>{const{path:o}=await e.recordStart({force:t.force===!0,path:Zn(n)});return{path:o}},Kn=async({control:e})=>{const{path:t}=await e.recordStop();return{path:t}};function Zn(e){const t=be(X(e,{extension:".mp4",strict:!0})),n=Lt(t);if(!ye(n))throw new r(`No such directory: ${n}`);return t}const Wn=async({device:e,positionals:[t]})=>{if(!Vn(t))throw new r(`Orientation must be one of: ${L.join(", ")}`);await e.rotate(t)};function Vn(e){return L.some(t=>t===e)}const Hn=ie(),Xn=async({device:e,options:t,positionals:[n]})=>{const o=await e.screenshot(Ne(t),ne(t)),i=Yn(o.mimeType,n);return await Se(i,o.data),{bytes:o.data.byteLength,output:i}};function Jn(e){const t=e.split("/").at(1);return`.${t==="jpeg"?"jpg":t??"png"}`}function Yn(e,t="screenshot"){return X(t,{extension:Jn(e)})}function Qn(e){const t=Oe(new URL("./daemon.js",import.meta.url)),n=It(dt(e.sessionId),"a"),o=zt(process.execPath,[t],{detached:!0,env:{...process.env,[ut]:ft(e),DEBUG:eo(process.env.DEBUG)},stdio:["ignore",n,n]});if(Tt(n),o.unref(),o.pid===void 0)throw new Error("Failed to spawn the session daemon");return o.pid}function eo(e){return e?`${e},appetize:daemon`:"appetize:daemon"}const to=/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/i;function no(){return Rt({capitalize:!1,separator:"-"})}function oo(e){if(!to.test(e))throw new r('--session-id must use letters, digits, ".", "-" or "_"');return e}const Fe=1e4;function Ce(e){if(!mt(e))return!1;try{process.kill(e.pid,"SIGTERM")}catch{}return!0}async function Me(e,t){(Ce(e)||so(e))&&await io(e,t)}async function io({pid:e,sessionId:t},n){const o=me(e);try{await q(Fe,AbortSignal.any([n,o]))}catch(i){if(o.aborted||!fe(e))return;throw i}throw new G(`Session ${t} did not stop in time`,{sessionId:t})}function so({endedAt:e,pid:t}){return e!==void 0&&Date.now()-Date.parse(e)<Fe&&fe(t)}const ao=["device-android-shell","device-appearance","device-font-size","device-ios-keyboards","device-locale","device-location","device-orientation","device-os-version","device-timezone"];function ro(e,t){const n=C({shell:f(e["device-android-shell"],"--device-android-shell"),softKeyboard:M(e["device-android-soft-keyboard"],{defaultValue:!0})}),o=C({autocorrect:M(e["device-ios-autocorrect"],{defaultValue:!0}),keyboards:I(e["device-ios-keyboards"],"--device-ios-keyboards")});return{android:n,appearance:x(e["device-appearance"],N,"--device-appearance"),fontSize:x(e["device-font-size"],D,"--device-font-size"),id:t,ios:o,locale:f(e["device-locale"],"--device-locale"),location:Be(e["device-location"],"--device-location"),orientation:x(e["device-orientation"],L,"--device-orientation"),osVersion:f(e["device-os-version"],"--device-os-version"),timezone:f(e["device-timezone"],"--device-timezone")}}function Be(e,t){const n=f(e,t);if(n===void 0)return;const o=n.split(",").map(a=>a.trim());if(o.length!==2||!o.every(co))throw new r(`${t} expects <lat>,<lng>, e.g., ${t}=-43.65,79.38`);const[i,s]=o.map(Number);if(Math.abs(i)>90||Math.abs(s)>180)throw new r(`${t} expects latitude within \xB190 and longitude within \xB1180`);return{latitude:i,longitude:s}}function co(e){return e!==""&&Number.isFinite(Number(e))}const qe={"device.id":"<device>","device.location.latitude":"--device-location latitude","device.location.longitude":"--device-location longitude"};function po(e,{endpointFromEnv:t=!1}={}){const n=t?{...qe,endpoint:"APPETIZE_ENDPOINT"}:qe;try{ht(e)}catch(o){throw o instanceof gt?new r(o.issues.map(i=>lo(i,n)).join(`
24
+ `)):o}}function lo({code:e,message:t,path:n},o){const i=uo(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 uo(e,t){return t[e]??`--${e.split(".").filter(n=>!/^\d+$/.test(n)).map(n=>n.replaceAll(/([A-Z])/g,"-$1").toLowerCase()).join("-")}`}const fo=[...ao,...Mt,"proxy","target"],mo=150,ho={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"},go=[{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 wo(e,{onPhase:t,signal:n}){let o;for(;;){const i=he(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 q(mo,n)}}function yo(e){if(!(e===void 0||e===!1)){if(e===""||e==="intercept")return"intercept";if(typeof e!="string"||!xo(e))throw new r("--proxy expects 'intercept' or an http:// url, e.g., http://proxy:8080");return e}}async function vo({options:e,positionals:t,progress:n,signal:o}){const[i]=t,s=f(e.target,"--target")??So(e);if(s===void 0&&!process.env.APPETIZE_API_TOKEN)throw new r("APPETIZE_API_TOKEN must be set without a target",["Set APPETIZE_API_TOKEN, or pass --target"]);const a=f(e["session-id"],"--session-id"),l=e.force===!0;if(l&&a===void 0)throw new r("--force needs --session-id");const c=a===void 0?no():oo(a),u={device:ro(e,i),endpoint:yt(e),featureFlags:wt(),launch:Bt(e),proxy:yo(e.proxy),sessionId:c,target:s};po(u,{endpointFromEnv:e.endpoint===void 0});const m=he(c);if(m!==void 0){if(!l)throw new vt(c);n.status(`Replacing session ${c}`),await Me(m,o),xt(c)}Qn(u),n.step(`Session ${c}`);const v=bt(c,u.proxy);if(e.wait===!1)return{logs:v,sessionId:c};const P=await wo(c,{onPhase:$=>{bo($)&&n.status(ho[$]??"Starting the session")},signal:o}).catch($=>{throw G.wrap($,c)});return{adbSerial:P.adbSerial,baseUrl:P.baseUrl,controlSocket:P.controlSocket,logs:v,sessionId:c,viewerUrl:P.viewerUrl}}function xo(e){return e.startsWith("http://")}function bo(e){return e!=="ended"&&e!=="error"}function So(e){const t=e._;return Array.isArray(t)&&typeof t[0]=="string"?t[0]:void 0}const Eo=async({options:e,progress:t,record:n,signal:o})=>(e.wait===!1?Ce(n):(t.status(`Stopping session ${n.sessionId}`),await Me(n,o)),{sessionId:n.sessionId}),Oo=["appearance","font-size","location"],Ao=async({device:e,options:t})=>{const n=x(t.appearance,N,"--appearance"),o=x(t["font-size"],D,"--font-size"),i=Be(t.location,"--location");if(!n&&!o&&!i)throw new r("Pass at least one of --appearance, --font-size or --location");n&&await e.settings.setAppearance(n),o&&await e.settings.setFontSize(o),i&&await e.settings.setLocation(i)},ko=async({device:e})=>{await e.shake()},Ge="appetize",Po=Oe(new URL(`./skills/${Ge}/`,import.meta.url)),ae=["agents","claude","codex","copilot","cursor"],Ke=["project","user"],$o={agents:".agents",claude:".claude",codex:".codex",copilot:".copilot",cursor:".cursor"},Io=b(".agents","skills"),To=["claude","agents"];function Lo(e){return ae.some(t=>t===e)}function Do(e){return Ke.some(t=>t===e)}async function No(e){const t=e.agents??await _o(e),n=await Promise.all(t.map(o=>Ze(o,e)));return[...new Set(n.flat())]}async function Ze(e,t){const n=We(e,t);if(e==="claude")return[b(n,"skills")];const o=b(t.root,Io);return t.scope==="project"?[o]:await Ve(n)?[o,b(n,"skills")]:[o]}function We(e,t){if(e==="claude"&&t.scope==="user"){const n=process.env.CLAUDE_CONFIG_DIR?.trim();if(n)return be(n)}return b(t.root,$o[e])}async function _o(e){const n=(await Promise.all(ae.map(async o=>await zo(o,e)?o:void 0))).filter(o=>o!==void 0);return n.length>0?n:To}async function zo(e,t){const n=[We(e,t),...await Ze(e,t)];return(await Promise.all(n.map(i=>Ve(i)))).includes(!0)}async function Ve(e){try{return(await Ee(e)).isDirectory()}catch{return!1}}const Ro=["agent","scope"];async function jo({options:e,signal:t}){const n=Co(e.scope),o=Fo(e.agent),i=n==="user"?jt():process.cwd(),s=await No({agents:o,root:i,scope:n}),a=[];for(const l of s){t.throwIfAborted();const c=b(l,Ge);await Nt(c,{force:!0,recursive:!0}),await _t(Po,c,{recursive:!0}),a.push(n==="user"?c:Dt(i,c))}return{installed:a}}function Uo(e){const t=T(e,"--agent");if(!Lo(t))throw new r(`--agent expects one of ${ae.join(", ")}`);return t}function Fo(e){return e===void 0?void 0:(Array.isArray(e)?e:[e]).map(n=>Uo(n))}function Co(e){if(e===void 0)return"project";const t=T(e,"--scope");if(!Do(t))throw new r(`--scope expects one of ${Ke.join(", ")}`);return t}const w="from",y="to",Mo=[...oe({prefix:w}),...oe({prefix:y})],Bo=async({device:e,options:t})=>{await e.swipe(Zo(t),Vo(t))};function qo(e){return W.some(t=>t===e)}function Go(e){if(e===void 0)return;const t=T(e,"--direction");if(!qo(t))throw new r(`--direction expects one of ${W.join(", ")}`);return t}function Ko(e,t){const n=St(e.distance,"--distance");if(R(e,{prefix:y})!==void 0)throw new r(`--direction may not be combined with --${y}-*`);return{direction:t,distance:n,from:R(e,{prefix:w})}}function Zo(e){const t=Go(e.direction);return t===void 0?Wo(e):Ko(e,t)}function Wo(e){if(e.distance!==void 0)throw new r("--distance is only valid with --direction");return{from:He(R(e,{prefix:w}),w),to:He(R(e,{prefix:y}),y)}}function Vo(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 He(e,t){if(e===void 0)throw new r(`A swipe needs --direction, or both endpoints: pass one of ${k({prefix:t})}`);return e}const Ho=oe(),Xo=async({device:e,options:t})=>{const n=_e(t);await e.tap(n,ne(t))},Jo=async({device:e,options:t,positionals:[n]})=>{await e.type(n,Yo(t))};function Yo(e){const t=h(e.delay,"--delay");return t===void 0?void 0:{delay:t}}const re=Ut("appetize:cli"),Qo={fail:()=>{},status:()=>{},step:()=>{},succeed:()=>{}};function ei(e,t,n){return e==="interactive"?ti(t,n):Qo}function ti(e,t){const n=Ft({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:a=>{if(!t.aborted){if(n.isSpinning){n.text=a;return}o=Date.now(),process.once("exit",i),n.start(a)}},step:a=>{e.write(`\u25B8 ${a}
25
+ `)},succeed:()=>{if(!s())return;const a=Math.round((Date.now()-o)/1e3);n.success(`${n.text} (${String(a)}s)`)}}}const ni=new Set(["adb","api","apk","id","os","url"]),oi={bottom:"","bottom-left":"","bottom-mid":"","bottom-right":"",left:"","left-mid":"",mid:"","mid-mid":"",middle:" ",right:"","right-mid":"",top:"","top-left":"","top-mid":"","top-right":""},Xe=2,ii=10;function si(e){return e.split(/(?=[A-Z])/).map((t,n)=>{const o=t.toLowerCase();return ni.has(o)?o.toUpperCase():n===0?o.charAt(0).toUpperCase()+o.slice(1):o}).join(" ")}function ai(e,t,n){return pe(e)?nt(e,t,n):U(e,{fields:t,key:""},n)}function ri(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 Je(e){return new Ct({chars:oi,head:e,style:{border:[],head:[],"padding-left":0,"padding-right":Xe}})}function Ye(e,t){return e.find(n=>n.key===t)}function Qe(e,t){return t?.header??si(e)}function ce(e){return e!=null}function pe(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function et(e){return["boolean","number","string"].includes(typeof e)}function tt(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 nt(e,t,n){const o=new Map(Object.entries(e)),i=tt(t,[...o.keys()]).filter(m=>ce(o.get(m))),s=i.map(m=>Ye(t,m)),a=i.map((m,v)=>Qe(m,s[v])),l=Math.max(0,...a.map(m=>m.length))+Xe,c=n===void 0?void 0:n-l-1,u=Je();return u.push(...i.map((m,v)=>[a[v],U(o.get(m),s[v],c)])),ot(u.toString())}function ci(e,t){const n=e.map(a=>new Map(Object.entries(a))),o=tt(t,n.flatMap(a=>[...a.keys()])),i=o.map(a=>Ye(t,a)),s=Je(o.map((a,l)=>Qe(a,i[l])));for(const a of n)s.push(o.map((l,c)=>{const u=a.get(l);return ce(u)?U(u,i[c]):""}));return ot(s.toString())}function U(e,t,n){if(t?.format)return F(t.format(e),n);if(et(e))return F(ge(e),n);if(Array.isArray(e)){const o=e.filter(ce);return o.every(et)?F(ge(o),n):o.every(pe)?ci(o,t?.fields??[]):o.map(i=>U(i,t,n)).join(`
26
+ `)}return pe(e)?nt(e,t?.fields??[],n):F(String(e),n)}function ot(e){return e.split(`
7
27
  `).map(t=>t.trimEnd()).join(`
8
- `)}function R(e,t){return t===void 0||t<Ko?e:e.split(`
9
- `).flatMap(n=>Ho(n,t)).join(`
10
- `)}function Ho(e,t){const n=[];let o="";for(const i of e.split(" ").flatMap(s=>Wo(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 Jo(e,t={}){const n=await fe(e,{signal:t.signal});return new ht(n,{...t,onClose:()=>{n.close(),t.onClose?.()}})}function Yo(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");return Jo(e.controlSocket,{baseUrl:e.baseUrl,signal:t})}async function Qo(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");const n=await fe(e.controlSocket,{signal:t}),{client:o}=gt(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 ei(e){return async({options:t,positionals:n,progress:o,signal:i})=>{const s=t["session-id"],r=wt(typeof s=="string"?s:void 0);try{return await e({options:t,positionals:n,progress:o,record:r,signal:AbortSignal.any([i,ce(r.pid)])})}catch(p){throw C.wrap(p,r.sessionId)}}}function ti(e){return async({options:t,positionals:n,progress:o,record:i,signal:s})=>{const r=await Yo(i,s);try{return await e({device:r,options:t,positionals:n,progress:o,signal:s})}finally{r.close()}}}function ni(e){return async({options:t,positionals:n,progress:o,record:i,signal:s})=>{const r=await Qo(i,s);try{return await e({control:r,options:t,positionals:n,progress:o,signal:s})}finally{r.close()}}}const oi=["session-id",...jt,...Bt,...Jt,...gn,...Eo,...Qn,...lo,...Io,...Ro],ii=["json","quiet"];function si(e){const t=Qe("appetize").version(e).option("--json","Print the result as JSON").option("--quiet","Hide progress");return E(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: ${D.join(" or ")}`).option("--device-font-size",`Device font size: ${T.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: ${$.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"),so,{fields:no}),se(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"),po),u(t,"biometry authenticate",n=>n.describe("Answer the app's biometric prompt").option("--no-match","Answer with a non-match").example("biometry authenticate").example("biometry authenticate --no-match"),Rt),u(t,"biometry enroll",n=>n.describe("Enroll biometry (iOS only)").option("--unenroll","Unenroll instead",!1).example("biometry enroll").example("biometry enroll --unenroll"),Ut),E(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"),Mt,{fields:Ct}),E(t,"build upload <file>",n=>n.describe([`Upload a build (${V.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(Se)})`).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"'),Zt,{fields:Kt}),E(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"),Qt,{fields:Yt}),nn in le()&&u(t,"fold <hinge>",n=>n.describe(`Fold a foldable device: ${G.join(", ")}`).example("fold closed").example("fold fullyOpen"),on),u(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"),rn),u(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"),wn),u(t,"keychain clear",n=>n.describe("Clear the entire device keychain (iOS only)").example("keychain clear"),bn),u(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"),xn),u(t,"press <button>",n=>n.describe(`Press a device button: ${ve.join(", ")}; Android only: ${ye.join(", ")}`).example("press home").example("press volumeUp"),On),Je(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"),Pn),Je(t,"recording stop",n=>n.describe("Stop recording the device video and close the file").example("recording stop"),kn),u(t,"rotate <orientation>",n=>n.describe("Rotate the device to an orientation: portrait, landscapeLeft, landscapeRight or upsideDown").example("rotate portrait").example("rotate landscapeLeft"),$n),u(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"),Dn),u(t,"settings set",n=>n.describe("Change device settings; pass at least one flag").option("--appearance",`Device appearance: ${D.join(" or ")}`).option("--font-size",`Device font size: ${T.join(", ")}`).option("--location","Device location as <lat>,<lng>").example("settings set --appearance dark").example("settings set --font-size xl --location 43.65,-79.38").example("settings set --location=-33.87,151.21"),uo),u(t,"shake",n=>n.describe("Shake the device").example("shake"),fo),E(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"),Oo),u(t,"swipe",n=>Y(Y(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: ${A({prefix:w})}`,` ${A({prefix:y})}`]).option("--direction",`Direction to travel: ${K.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"),$o),u(t,"tap",n=>Y(n.describe(["Tap an element matched by a selector, or a screen position","",`Pick one of: ${A()}`])).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"),Uo),u(t,"type <text>",n=>n.describe(`Type text; {...} presses a named key: ${bt.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"),jo),t}function E(e,t,n,o,{fields:i}={}){const s=t.split(" ").filter(r=>!/^[<[]/.test(r)).join(" ");return n(e.command(t)).action(ci({action:o,fields:i,name:s}))}function se(e,t,n,o){return E(e,t,i=>n(i).option("--session-id","Target session id; defaults to the sole active session"),ei(o))}function u(e,t,n,o){return se(e,t,n,ti(o))}function Je(e,t,n,o){return se(e,t,n,ni(o))}function Ye(e){return`${String(Date.now()-e)}ms`}function ri(e,t,n){if(e==="json")return JSON.stringify(t===void 0?{}:t,null,2);const o=t===void 0?"":Vo(t,n,process.stdout.columns);return o===""?void 0:o}async function ai({action:e,fields:t=[],name:n},o){const i=Date.now();ne(`invoked: ${process.argv.slice(2).join(" ")}`);const s=yt(o),{format:r,progress:p}=ln(s.options,process.stdout,process.stderr),c=Mo(p,process.stderr,s.signal);try{const d=ri(r,await e({...s,progress:c}),t);c.succeed(),d!==void 0&&process.stdout.write(`${d}
11
- `),ne(`ok in ${Ye(i)}`)}catch(d){c.fail(),process.stderr.write(`${vt(r,d,{command:n})}
12
- `),ne(`failed after ${Ye(i)}:`,d),process.exitCode=1}}function ci(e){return(...t)=>{ai(e,t)}}si("0.27.0").parse(process.argv,{boolean:ii,string:oi});
28
+ `)}function F(e,t){return t===void 0||t<ii?e:e.split(`
29
+ `).flatMap(n=>pi(n,t)).join(`
30
+ `)}function pi(e,t){const n=[];let o="";for(const i of e.split(" ").flatMap(s=>ri(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 li(e,t={}){const n=await we(e,{signal:t.signal});return new Et(n,{...t,onClose:()=>{n.close(),t.onClose?.()}})}function di(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");return li(e.controlSocket,{baseUrl:e.baseUrl,signal:t})}async function ui(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");const n=await we(e.controlSocket,{signal:t}),{client:o}=Ot(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 fi(e){return async({options:t,positionals:n,progress:o,signal:i})=>{const s=t["session-id"],a=At(typeof s=="string"?s:void 0);try{return await e({options:t,positionals:n,progress:o,record:a,signal:AbortSignal.any([i,me(a.pid)])})}catch(l){throw G.wrap(l,a.sessionId)}}}function mi(e){return async({options:t,positionals:n,progress:o,record:i,signal:s})=>{const a=await di(i,s);try{return await e({device:a,options:t,positionals:n,progress:o,signal:s})}finally{a.close()}}}function hi(e){return async({options:t,positionals:n,progress:o,record:i,signal:s})=>{const a=await ui(i,s);try{return await e({control:a,options:t,positionals:n,progress:o,signal:s})}finally{a.close()}}}const gi=["session-id",...Zt,...en,...sn,...mn,...Ln,...Hn,...Ro,...fo,...Oo,...Mo,...Ho],wi=["json","quiet"];function yi(e){const t=at("appetize").version(e).option("--json","Print the result as JSON").option("--quiet","Hide progress");return E(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: ${N.join(" or ")}`).option("--device-font-size",`Device font size: ${D.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: ${L.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: ${de.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"),vo,{fields:go}),le(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"),Eo),p(t,"app clear",n=>n.describe("Wipe an app's data").option("--app","App id; defaults to every session app").example("app clear").example("app clear --app com.example.app"),Wt),p(t,"app launch",n=>n.describe(["Launch an app, or bring it to the foreground","","With --params or --ios-args, a running app is relaunched"]).option("--app","App id; defaults to the session's app").option("--ios-args","iOS only: launch argument, joined with = if it starts with -; repeatable").option("--params","Launch parameter as key=value, JSON when valid; repeatable").example("app launch").example("app launch --app com.apple.Preferences").example("app launch --params debug=true --params retries=3").example("app launch --ios-args=-debug --ios-args YES"),Vt),p(t,"app reinstall",n=>n.describe("Reinstall an app, then restart it").option("--app","Session app id; defaults to every session app").example("app reinstall").example("app reinstall --app com.example.app"),Ht),p(t,"app restart",n=>n.describe("Stop and relaunch an app").option("--app","App id; defaults to the last launched app").option("--ios-args","iOS only: launch argument, joined with = if it starts with -; repeatable").option("--params","Launch parameter as key=value, JSON when valid; repeatable").example("app restart").example("app restart --params debug=true"),Xt),p(t,"app terminate",n=>n.describe("Stop an app").option("--app","App id; defaults to every session app").example("app terminate").example("app terminate --app com.example.app"),Jt),p(t,"biometry authenticate",n=>n.describe("Answer the app's biometric prompt; on iOS, enroll with biometry enroll first").option("--no-match","Answer with a non-match").example("biometry authenticate").example("biometry authenticate --no-match"),Yt),p(t,"biometry enroll",n=>n.describe("Enroll biometry (iOS only)").option("--unenroll","Unenroll instead",!1).example("biometry enroll").example("biometry enroll --unenroll"),Qt),E(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"),on,{fields:nn}),E(t,"build upload <file>",n=>n.describe([`Upload a build (${H.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(Le)})`).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"'),pn,{fields:cn}),E(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"),gn,{fields:hn}),p(t,"fold <hinge>",n=>n.describe(`Fold a foldable device: ${K.join(", ")}`).example("fold closed").example("fold fullyOpen"),vn),p(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"),bn),p(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"),Dn),p(t,"keychain clear",n=>n.describe("Clear the entire device keychain (iOS only)").example("keychain clear"),zn),p(t,"media add <path>",n=>n.describe("Add a photo or video to the device's gallery").example("media add photo.png").example("media add ./clips/demo.mp4"),Rn),p(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"),Fn),p(t,"press <button>",n=>n.describe(`Press a device button: ${ke.join(", ")}; Android only: ${Ae.join(", ")}`).example("press home").example("press volumeUp"),Bn),it(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"),Gn),it(t,"recording stop",n=>n.describe("Stop recording the device video and close the file").example("recording stop"),Kn),p(t,"rotate <orientation>",n=>n.describe("Rotate the device to an orientation: portrait, landscapeLeft, landscapeRight or upsideDown").example("rotate portrait").example("rotate landscapeLeft"),Wn),p(t,"screenshot [output]",n=>ee(n.describe("Take a screenshot of the screen, or of one element")).option("--timeout","Milliseconds to wait for a match").example("screenshot").example("screenshot home-screen").example("screenshot ./shots/checkout.png").example("screenshot card --select-test-id card"),Xn),p(t,"settings set",n=>n.describe("Change device settings; pass at least one flag").option("--appearance",`Device appearance: ${N.join(" or ")}`).option("--font-size",`Device font size: ${D.join(", ")}`).option("--location","Device location as <lat>,<lng>").example("settings set --appearance dark").example("settings set --font-size xl --location 43.65,-79.38").example("settings set --location=-33.87,151.21"),Ao),p(t,"shake",n=>n.describe("Shake the device").example("shake"),ko),E(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"),jo),p(t,"swipe",n=>te(te(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: ${k({prefix:w})}`,` ${k({prefix:y})}`]).option("--direction",`Direction to travel: ${W.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"),Bo),p(t,"tap",n=>te(n.describe(["Tap an element matched by a selector, or a screen position","",`Pick one of: ${k()}`])).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"),Xo),p(t,"type <text>",n=>n.describe(`Type text; {...} presses a named key: ${$t.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"),Jo),vi(t,$n)}function vi(e,t){const n=e.help.bind(e);return e.help=o=>{n(o),o||process.stdout.write(t)},e}function E(e,t,n,o,{fields:i}={}){const s=t.split(" ").filter(a=>!/^[<[]/.test(a)).join(" ");return n(e.command(t)).action(Si({action:o,fields:i,name:s}))}function le(e,t,n,o){return E(e,t,i=>n(i).option("--session-id","Target session id; defaults to the sole active session"),fi(o))}function p(e,t,n,o){return le(e,t,n,mi(o))}function it(e,t,n,o){return le(e,t,n,hi(o))}function st(e){return`${String(Date.now()-e)}ms`}function xi(e,t,n){if(e==="json")return JSON.stringify(t===void 0?{}:t,null,2);const o=t===void 0?"":ai(t,n,process.stdout.columns);return o===""?void 0:o}async function bi({action:e,fields:t=[],name:n},o){const i=Date.now();re(`invoked: ${process.argv.slice(2).join(" ")}`);const s=kt(o),{format:a,progress:l}=An(s.options,process.stdout,process.stderr),c=ei(l,process.stderr,s.signal);try{const u=xi(a,await e({...s,progress:c}),t);c.succeed(),u!==void 0&&process.stdout.write(`${u}
31
+ `),re(`ok in ${st(i)}`)}catch(u){c.fail(),process.stderr.write(`${Pt(a,u,{command:n})}
32
+ `),re(`failed after ${st(i)}:`,u),process.exitCode=1}}function Si(e){return(...t)=>{bi(e,t)}}yi("0.29.0").parse(process.argv,{boolean:wi,string:gi});
package/daemon.js CHANGED
@@ -1,2 +1,2 @@
1
- import{T as _,P as A,Q as F,R as T,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-Cbdd68uZ.js";import b 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,Mp4OutputFormat as le,EncodedPacket as fe,FilePathTarget as pe}from"mediabunny";import"json-rpc-2.0";import"zod/mini";import"node:path";import"node:os";import"stream";import"module";const he=b("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=b("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=T(new Uint8Array(t.data));return z(n)?n:null}catch{return null}}}function S(e){return typeof e=="object"&&e!==null}function I(e){try{const t=T(new Uint8Array(e.data));if(S(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 ye(o,r,t),r}function ye(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)}})}const me=b("appetize:client:render-video-to-mp4"),k=30;async function be(e,t){await e.pipeTo(Ee(t))}function Se(e){return{decoderConfig:{codec:"avc1.640028",codedHeight:e.height,codedWidth:e.width}}}function Ee(e){let t;const n=async()=>{await t?.output.finalize()};return new WritableStream({abort:n,close:n,write:async o=>{if(o.data.byteLength!==0)try{t??=await Te(ke(o),e()),Le(t,o),await Ae(t,o)}catch(r){throw await t?.output.cancel(),r}}})}async function Te(e,t){const n=new de("avc"),o=new ue({format:new le({fastStart:"fragmented"}),target:t});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 Le(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},me(`video resized to ${String(t.width)}x${String(t.height)}; keeping ${String(o.width)}x${String(o.height)}`))}function ke(e){if(e.type!=="key")throw new Error("Video stream started without a key chunk");return e}async function Ae(e,t){const n=new fe(t.data,t.type,e.frame/k,1/k);e.frame+=1,await e.source.add(n,Se(e.size))}async function $e(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=Re(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 Re(e){if(!e)return;if(e.name==="ready")return{phase:"ready"};if(e.name!=="launchStatus")return;switch(S(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 Pe(){return typeof process>"u"?void 0:process}function Ie(e){return e==="intercept"?"interceptOnDemand":e}function We(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=>{Ue(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 Ue(e,t,n){const o=T(new Uint8Array(t.data));S(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 Ce(e,t,{apiToken:n,onQueue:o,signal:r}={}){const s=N();s.promise.catch(()=>{}),Oe(s.reject,r);const i=await We(xe(e),{onClose:()=>{s.reject(De())},onEvent:(c,a)=>{Me(c,a,s,o)},signal:r});return qe(e,{apiToken:n,data:t,socketid:i.id},r).catch(s.reject),await s.promise.finally(()=>{i.close()})}function De(){return new v("disconnected","The webserver connection closed before a session was granted")}function U(e,t){return S(e)?e[t]:void 0}function Me(e,t,n,o){if(e==="queue"||e==="concurrentQueue"){const r=e==="queue"?Fe(t):ze(t);r&&o?.(r)}else e==="newSession"?_e(n,t):e==="userError"&&n.reject(new v("denied",Ve(t)))}function C(e,t){const n=U(e,t);return typeof n=="number"?n:void 0}function y(e,t){const n=U(e,t);return typeof n=="string"?n:void 0}async function qe(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 je(o))}function Oe(e,t){t?.addEventListener("abort",()=>{e(t.reason)},{once:!0})}function xe(e){const t=new URL("/client",e);return t.protocol=t.protocol==="http:"?"ws:":"wss:",t.href}function _e(e,t){const n=y(t,"path"),o=y(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 Fe(e){const t=C(e,"position");return t===void 0?void 0:{position:t,reason:"capacity"}}function ze(e){const t=C(e,"position");if(t!==void 0)return{limit:y(e,"name")??"Queue",position:t,reason:"concurrency"}}function Ve(e){return y(e,"message")??"The session request was denied"}async function je(e){const t=`The webserver rejected the session request (${String(e.status)})`,n=await e.json().catch(()=>{}),o=y(n,"message");return o?`${t}: ${o}`:t}async function Ne({apiToken:e=He(),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 Ce(o,{codec:t,noVideo:!0,proxy:Ie(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:Qe(d)}}function He(){return Pe()?.env.APPETIZE_API_TOKEN}function Qe(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 Je(e,t){await be(e,()=>new pe(t,{chunked:!1}))}async function Ge(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 f=await e.debug.createAdbTunnel();if(l.destroyed){f.close();return}u.tunnel=f,f.closed.catch(E=>{n?.(E)}),Ke(l,f)}catch(f){l.destroyed||n?.(f),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 Ke(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 Xe(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 Ye(s,n),o?.aborted&&(i(),o.throwIfAborted()),o?.addEventListener("abort",i,{once:!0}),{close:i,path:n}}function Ye(e,t){return new Promise((n,o)=>{e.once("error",o),e.listen(t,()=>{e.off("error",o),n()})})}const Be=b("appetize:daemon");function p(e){Be(e)}function et(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 tt=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),M=e=>{throw TypeError(e)},nt=(e,t,n)=>{if(t!=null){typeof t!="object"&&typeof t!="function"&&M("Object expected");var o,r;o===void 0&&(o=t[tt("dispose")]),typeof o!="function"&&M("Object not disposable"),r&&(o=function(){try{r.call(this)}catch(s){return Promise.reject(s)}}),e.push([n,o,t])}return t},ot=(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 rt=1e4;async function st(e,t,{onClose:n,signal:o}={}){var r=[];try{const c=nt(r,et(rt,{message:"Timed out waiting for session request state"})),a=AbortSignal.any([o,c.signal].filter(g=>!!g)),d=g=>{t.save({state:g}),p(g.phase),g.phase!=="requesting"&&c.cancel()},{featureFlags:h,sessionId:u,...l}=e,{baseUrl:f,url:E}=await Ne({...l,onStateChange:d,signal:a});t.save({baseUrl:f});const m=new L(E,{signal:a});return await $e(m,{onStateChange:d,signal:a}),m.onclose=()=>{n?.("The device connection closed")},{device:await ve(m,{baseUrl:f,signal:a}),transport:m}}catch(c){var s=c,i=!0}finally{ot(r,s,i)}}function it(){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 at(e,t,n,{signal:o}={}){try{if(!await ct(t))return;const r=await Ge(e,{onError:s=>{p(`adb endpoint error: ${q(s)}`)},signal:o});n.save({adbSerial:r.url.host}),p(`adb endpoint listening at ${r.url.host}`)}catch(r){p(`adb endpoint unavailable: ${q(r)}`)}}async function ct(e){return await Y(e.device.id,{endpoint:e.endpoint})==="android"}function q(e){return e instanceof Error?e.message:String(e)}async function dt(e,t,n,{recorder:o,signal:r}){const{path:s}=await Xe(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 ut(e,t,n={}){try{const o=await lt(e,n);return t.save({viewerUrl:o.url.href}),p(`web endpoint listening at ${o.url.href}`),o}catch(o){p(`web endpoint unavailable: ${gt(o)}`);return}}async function lt(e,{port:t=0,signal:n,viewerHtml:o=ne(new URL("./viewer.html",import.meta.url),"utf8")}){n?.throwIfAborted();const r=ie((u,l)=>{pt(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,f)=>{ht(i,u,l,f)});const a=()=>(c??=new Promise((u,l)=>{n?.removeEventListener("abort",d);for(const f of s)ft(f);i.close(),r.close(f=>{f?l(f):u()})}),c),d=()=>{a()};await wt(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 ft(e){e.close(1001)}function pt(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 ht(e,t,n,o){if(t.url!=="/ws"){n.destroy();return}e.handleUpgrade(t,n,o,r=>{e.emit("connection",r,t)})}function wt(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 gt(e){return e instanceof Error?e.message:String(e)}function vt(e){const t=new AbortController,n=yt(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 yt(e,t){let n=!1;return o=>{n||(n=!0,e.save({endedAt:new Date().toISOString(),endedReason:o,state:{phase:"ended"}}),p(o),t.abort())}}async function x(e,t,{label:n,signal:o}){try{const r=await e();await ce(r.readable.pipeThrough(mt()),oe(t,{flags:"a"}))}catch(r){if(o?.aborted)return;const s=r instanceof Error?r.message:String(r);p(`${n} ended with an error: ${s}`)}}function mt(){return new TransformStream({transform:(e,t)=>{t.enqueue(`${JSON.stringify(e)}
1
+ import{T as _,P as A,Q as F,R as T,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-BByjqBT3.js";import b 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,Mp4OutputFormat as le,EncodedPacket as fe,FilePathTarget as pe}from"mediabunny";import"json-rpc-2.0";import"zod/mini";import"node:path";import"node:os";import"stream";import"module";const he=b("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=b("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=T(new Uint8Array(t.data));return z(n)?n:null}catch{return null}}}function S(e){return typeof e=="object"&&e!==null}function I(e){try{const t=T(new Uint8Array(e.data));if(S(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 ye(o,r,t),r}function ye(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)}})}const me=b("appetize:client:render-video-to-mp4"),k=30;async function be(e,t){await e.pipeTo(Ee(t))}function Se(e){return{decoderConfig:{codec:"avc1.640028",codedHeight:e.height,codedWidth:e.width}}}function Ee(e){let t;const n=async()=>{await t?.output.finalize()};return new WritableStream({abort:n,close:n,write:async o=>{if(o.data.byteLength!==0)try{t??=await Te(ke(o),e()),Le(t,o),await Ae(t,o)}catch(r){throw await t?.output.cancel(),r}}})}async function Te(e,t){const n=new de("avc"),o=new ue({format:new le({fastStart:"fragmented"}),target:t});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 Le(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},me(`video resized to ${String(t.width)}x${String(t.height)}; keeping ${String(o.width)}x${String(o.height)}`))}function ke(e){if(e.type!=="key")throw new Error("Video stream started without a key chunk");return e}async function Ae(e,t){const n=new fe(t.data,t.type,e.frame/k,1/k);e.frame+=1,await e.source.add(n,Se(e.size))}async function $e(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=Re(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 Re(e){if(!e)return;if(e.name==="ready")return{phase:"ready"};if(e.name!=="launchStatus")return;switch(S(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 Pe(){return typeof process>"u"?void 0:process}function Ie(e){return e==="intercept"?"interceptOnDemand":e}function We(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=>{Ue(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 Ue(e,t,n){const o=T(new Uint8Array(t.data));S(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 Ce(e,t,{apiToken:n,onQueue:o,signal:r}={}){const s=N();s.promise.catch(()=>{}),Oe(s.reject,r);const i=await We(xe(e),{onClose:()=>{s.reject(De())},onEvent:(c,a)=>{Me(c,a,s,o)},signal:r});return qe(e,{apiToken:n,data:t,socketid:i.id},r).catch(s.reject),await s.promise.finally(()=>{i.close()})}function De(){return new v("disconnected","The webserver connection closed before a session was granted")}function U(e,t){return S(e)?e[t]:void 0}function Me(e,t,n,o){if(e==="queue"||e==="concurrentQueue"){const r=e==="queue"?Fe(t):ze(t);r&&o?.(r)}else e==="newSession"?_e(n,t):e==="userError"&&n.reject(new v("denied",Ve(t)))}function C(e,t){const n=U(e,t);return typeof n=="number"?n:void 0}function y(e,t){const n=U(e,t);return typeof n=="string"?n:void 0}async function qe(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 je(o))}function Oe(e,t){t?.addEventListener("abort",()=>{e(t.reason)},{once:!0})}function xe(e){const t=new URL("/client",e);return t.protocol=t.protocol==="http:"?"ws:":"wss:",t.href}function _e(e,t){const n=y(t,"path"),o=y(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 Fe(e){const t=C(e,"position");return t===void 0?void 0:{position:t,reason:"capacity"}}function ze(e){const t=C(e,"position");if(t!==void 0)return{limit:y(e,"name")??"Queue",position:t,reason:"concurrency"}}function Ve(e){return y(e,"message")??"The session request was denied"}async function je(e){const t=`The webserver rejected the session request (${String(e.status)})`,n=await e.json().catch(()=>{}),o=y(n,"message");return o?`${t}: ${o}`:t}async function Ne({apiToken:e=He(),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 Ce(o,{codec:t,noVideo:!0,proxy:Ie(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:Qe(d)}}function He(){return Pe()?.env.APPETIZE_API_TOKEN}function Qe(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 Je(e,t){await be(e,()=>new pe(t,{chunked:!1}))}async function Ge(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 f=await e.debug.createAdbTunnel();if(l.destroyed){f.close();return}u.tunnel=f,f.closed.catch(E=>{n?.(E)}),Ke(l,f)}catch(f){l.destroyed||n?.(f),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 Ke(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 Xe(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 Ye(s,n),o?.aborted&&(i(),o.throwIfAborted()),o?.addEventListener("abort",i,{once:!0}),{close:i,path:n}}function Ye(e,t){return new Promise((n,o)=>{e.once("error",o),e.listen(t,()=>{e.off("error",o),n()})})}const Be=b("appetize:daemon");function p(e){Be(e)}function et(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 tt=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),M=e=>{throw TypeError(e)},nt=(e,t,n)=>{if(t!=null){typeof t!="object"&&typeof t!="function"&&M("Object expected");var o,r;o===void 0&&(o=t[tt("dispose")]),typeof o!="function"&&M("Object not disposable"),r&&(o=function(){try{r.call(this)}catch(s){return Promise.reject(s)}}),e.push([n,o,t])}return t},ot=(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 rt=1e4;async function st(e,t,{onClose:n,signal:o}={}){var r=[];try{const c=nt(r,et(rt,{message:"Timed out waiting for session request state"})),a=AbortSignal.any([o,c.signal].filter(g=>!!g)),d=g=>{t.save({state:g}),p(g.phase),g.phase!=="requesting"&&c.cancel()},{featureFlags:h,sessionId:u,...l}=e,{baseUrl:f,url:E}=await Ne({...l,onStateChange:d,signal:a});t.save({baseUrl:f});const m=new L(E,{signal:a});return await $e(m,{onStateChange:d,signal:a}),m.onclose=()=>{n?.("The device connection closed")},{device:await ve(m,{baseUrl:f,signal:a}),transport:m}}catch(c){var s=c,i=!0}finally{ot(r,s,i)}}function it(){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 at(e,t,n,{signal:o}={}){try{if(!await ct(t))return;const r=await Ge(e,{onError:s=>{p(`adb endpoint error: ${q(s)}`)},signal:o});n.save({adbSerial:r.url.host}),p(`adb endpoint listening at ${r.url.host}`)}catch(r){p(`adb endpoint unavailable: ${q(r)}`)}}async function ct(e){return await Y(e.device.id,{endpoint:e.endpoint})==="android"}function q(e){return e instanceof Error?e.message:String(e)}async function dt(e,t,n,{recorder:o,signal:r}){const{path:s}=await Xe(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 ut(e,t,n={}){try{const o=await lt(e,n);return t.save({viewerUrl:o.url.href}),p(`web endpoint listening at ${o.url.href}`),o}catch(o){p(`web endpoint unavailable: ${gt(o)}`);return}}async function lt(e,{port:t=0,signal:n,viewerHtml:o=ne(new URL("./viewer.html",import.meta.url),"utf8")}){n?.throwIfAborted();const r=ie((u,l)=>{pt(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,f)=>{ht(i,u,l,f)});const a=()=>(c??=new Promise((u,l)=>{n?.removeEventListener("abort",d);for(const f of s)ft(f);i.close(),r.close(f=>{f?l(f):u()})}),c),d=()=>{a()};await wt(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 ft(e){e.close(1001)}function pt(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 ht(e,t,n,o){if(t.url!=="/ws"){n.destroy();return}e.handleUpgrade(t,n,o,r=>{e.emit("connection",r,t)})}function wt(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 gt(e){return e instanceof Error?e.message:String(e)}function vt(e){const t=new AbortController,n=yt(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 yt(e,t){let n=!1;return o=>{n||(n=!0,e.save({endedAt:new Date().toISOString(),endedReason:o,state:{phase:"ended"}}),p(o),t.abort())}}async function x(e,t,{label:n,signal:o}){try{const r=await e();await ce(r.readable.pipeThrough(mt()),oe(t,{flags:"a"}))}catch(r){if(o?.aborted)return;const s=r instanceof Error?r.message:String(r);p(`${n} ended with an error: ${s}`)}}function mt(){return new TransformStream({transform:(e,t)=>{t.enqueue(`${JSON.stringify(e)}
2
2
  `)}})}async function bt(e,t,{signal:n}={}){await x(async()=>await e.debug.createLogStream({signal:n}),t,{label:"log capture",signal:n})}async function St(e,t,{signal:n}={}){await x(async()=>await e.debug.createProxyInterceptStream({signal:n}),t,{label:"network capture",signal:n})}function Et(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:Tt(h,a),path:a,stream:h},t.save({video:{path:a,recording:!0}}),p(`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),p(`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 Tt(e,t){try{await Je(e.readable,t)}catch(n){p(`video capture ended with an error: ${String(n)}`)}}async function Lt(){const{config:e,logs:t,store:n}=it();p(`recording to ${n.path}`);const{shutdown:o,signal:r}=vt(n);try{const{device:s,transport:i}=await st(e,n,{onClose:o,signal:r}),c=Et(s,n,{signal:r});await at(s,e,n,{signal:r}),await ut(i,n,{signal:r});const a=await dt(i,n,e.sessionId,{recorder:c,signal:r});p(`control socket listening at ${a}`),bt(s,t.device,{signal:r}),p(`recording device logs to ${t.device}`),t.network&&(St(s,t.network,{signal:r}),p(`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"}}),p(`error: ${i}`),process.exitCode=1}}Lt();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appetize/cli",
3
- "version": "0.27.0",
3
+ "version": "0.29.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",
@@ -32,7 +32,7 @@
32
32
  "debug": "^4.4.3",
33
33
  "human-id": "^4.2.2",
34
34
  "json-rpc-2.0": "^1.8.1",
35
- "mediabunny": "^1.62.0",
35
+ "mediabunny": "^1.64.0",
36
36
  "sade": "^1.8.1",
37
37
  "ws": "^8.22.0",
38
38
  "yocto-spinner": "^1.2.2",
@@ -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.27.0'
13
+ cli-version: '0.29.0'
14
14
  ---
15
15
 
16
16
  # Appetize
@@ -117,7 +117,7 @@ appetize screenshot after-login --json
117
117
 
118
118
  Screenshot after each meaningful step. A tap that "succeeded" only means the
119
119
  gesture was delivered — the app may have shown an error, stayed put, or
120
- navigated somewhere unexpected.
120
+ navigated somewhere unexpected. Add `--select-*` to capture one element.
121
121
 
122
122
  ### 4. Wait by inspecting, not by sleeping
123
123
 
@@ -208,6 +208,18 @@ destination with `inspect --timeout`. A custom scheme needs its app installed.
208
208
  On iOS, `appetize keychain clear` clears the keychain without restarting the
209
209
  session.
210
210
 
211
+ ## Apps
212
+
213
+ Control the session's app without restarting the session:
214
+
215
+ ```bash
216
+ appetize app restart --params debug=true --json
217
+ appetize app clear --json
218
+ appetize app reinstall --json
219
+ ```
220
+
221
+ `--app <id>` targets another installed app; `reinstall` takes only a session app.
222
+
211
223
  ## Device settings
212
224
 
213
225
  Change settings mid-session instead of restarting:
@@ -220,12 +232,23 @@ appetize biometry enroll --json
220
232
  ## Biometry
221
233
 
222
234
  `appetize biometry authenticate` answers the app's biometric prompt with a
223
- match; `--no-match` answers with a non-match.
235
+ match; `--no-match` answers with a non-match. On iOS, run
236
+ `appetize biometry enroll` first; Android images come enrolled.
237
+
238
+ ## Media
239
+
240
+ `appetize media add <path>` adds a PNG, JPEG, GIF or MP4 of up to 50 MB to
241
+ the device's gallery under its file name.
224
242
 
225
243
  ## Shake
226
244
 
227
245
  `appetize shake` shakes the device.
228
246
 
247
+ ## Fold
248
+
249
+ `appetize fold <hinge>` folds a foldable device: `closed`, `partiallyOpen`
250
+ or `fullyOpen`.
251
+
229
252
  ## Full command reference
230
253
 
231
254
  Run `appetize <command> --help` for flags. `references/commands.md` covers
@@ -48,9 +48,22 @@ Session directories live under `~/.appetize/cli/sessions/`, or
48
48
  Waits for the daemon to exit, so logs are complete; `--no-wait` only signals
49
49
  it. Safe to call twice.
50
50
 
51
+ ## app launch, app restart
52
+
53
+ Without `--app`, `launch` acts on the session's app and `restart` on the last
54
+ launched one. `--params` and, on iOS, `--ios-args` apply to this launch only,
55
+ taking the same values as `session start`'s `--launch-params` and
56
+ `--launch-ios-args`. `launch` with either relaunches a running app.
57
+
58
+ ## app terminate, app reinstall, app clear
59
+
60
+ Without `--app`, these act on every session app. `reinstall` takes only a
61
+ session app, and restarts it afterwards.
62
+
51
63
  ## biometry authenticate
52
64
 
53
- Fails when the device has no biometry enrolled.
65
+ Fails when the device has no biometry enrolled. iOS starts unenrolled, so run
66
+ `biometry enroll` first; Android images come enrolled.
54
67
 
55
68
  ## biometry enroll
56
69
 
@@ -72,6 +85,10 @@ for them.
72
85
  Needs no `APPETIZE_API_TOKEN`. `osVersions` are the values
73
86
  `--device-os-version` accepts.
74
87
 
88
+ ## fold
89
+
90
+ Foldable devices only; others fail with the device's error.
91
+
75
92
  ## gesture
76
93
 
77
94
  `--help` holds the syntax and examples. Every element is resolved before the
@@ -92,6 +109,11 @@ Typing does not focus a field — tap it first. `{Backspace>16/}` repeats a key
92
109
  and `{{` types a literal `{`. An unclosed token or unknown key fails without
93
110
  typing anything.
94
111
 
112
+ ## media add
113
+
114
+ Fails with the server's message when the file isn't a PNG, JPEG, GIF or MP4,
115
+ or is over 50 MB.
116
+
95
117
  ## open
96
118
 
97
119
  A scheme-less value is refused. iOS caps URLs at 2048 characters.
@@ -128,11 +150,15 @@ Claude Code, `.agents/skills` for the others — and into both when none is.
128
150
 
129
151
  ## Selectors
130
152
 
131
- `inspect --select-*`, `tap --select-*` and `swipe --from-*`/`--to-*` share the
132
- same suffixes. `-text` is an exact string or `/regex/flags`; `-index` is
133
- 0-based. `-test-id` is an exact match: on Android the whole `resource-id` or the
134
- part after its last `/`, on iOS the `accessibilityIdentifier`.
153
+ Element selectors are flags ending in:
154
+
155
+ - `-text`: an exact string or `/regex/flags`.
156
+ - `-test-id`: an exact id; on Android the whole `resource-id` or the part after
157
+ its last `/`, on iOS the `accessibilityIdentifier`.
158
+ - `-index`: which match when several, 0-based.
159
+
160
+ Every element selector is also a position selector, at the element's center by
161
+ default:
135
162
 
136
- Only `tap` and `swipe` take positions. `-position` is a screen `x,y`, each 0-1;
137
- `-x` and `-y` place the point within the matched element, beyond its edge when
138
- outside 0-1.
163
+ - `-position`: a screen `x,y`, each 0-1.
164
+ - `-x` and `-y`: the point within the element, beyond its edge outside 0-1.