@appetize/cli 0.15.0 → 0.15.2

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,17 @@
1
1
  # @appetize/cli
2
2
 
3
+ ## 0.15.2
4
+
5
+ ### Patch Changes
6
+
7
+ - docs(cli): skill suggests `adb install` for iterating on an Android build
8
+
9
+ ## 0.15.1
10
+
11
+ ### Patch Changes
12
+
13
+ - docs(cli): link the README to the published CHANGELOG
14
+
3
15
  ## 0.15.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -7,4 +7,4 @@ npm install -g @appetize/cli
7
7
  appetize --help
8
8
  ```
9
9
 
10
- Release notes are in [CHANGELOG.md](./CHANGELOG.md), included in the published package.
10
+ Release notes are in the [CHANGELOG](https://cdn.jsdelivr.net/npm/@appetize/cli/CHANGELOG.md), also included in the published package.
package/cli.js CHANGED
@@ -16,4 +16,4 @@ import Se from"sade";import{d as U,D as ve,p as m,a as p,f as $,b as xe,i as Ee,
16
16
  `)}function eo(e){const t=v(e,"--agent");if(!Xt(t))throw new Error(`--agent expects one of ${R.join(", ")}`);return t}function to(e){return e===void 0?void 0:(Array.isArray(e)?e:[e]).map(o=>eo(o))}function oo(e){if(e===void 0)return"project";const t=v(e,"--scope");if(!Zt(t))throw new Error(`--scope expects one of ${de.join(", ")}`);return t}const l="from",u="to",no=[...L({prefix:l}),...L({prefix:u})],io=async({device:e,options:t})=>{await e.swipe(co(t),lo(t)),process.stdout.write(`${JSON.stringify({swiped:!0},null,2)}
17
17
  `)};function so(e){return I.some(t=>t===e)}function ro(e){if(e===void 0)return;const t=v(e,"--direction");if(!so(t))throw new Error(`--direction expects one of ${I.join(", ")}`);return t}function ao(e,t){const o=Te(e.distance,"--distance");if(E(e,{prefix:u})!==void 0)throw new Error(`--direction may not be combined with --${u}-*`);return{direction:t,distance:o,from:E(e,{prefix:l})}}function co(e){const t=ro(e.direction);return t===void 0?po(e):ao(e,t)}function po(e){if(e.distance!==void 0)throw new Error("--distance is only valid with --direction");return{from:me(E(e,{prefix:l}),l),to:me(E(e,{prefix:u}),u)}}function lo(e){const t=p(e.duration,"--duration"),o=p(e.timeout,"--timeout");return t===void 0&&o===void 0?void 0:{duration:t,timeout:o}}function me(e,t){if(e===void 0)throw new Error(`A swipe needs --direction, or both endpoints: pass one of ${S({prefix:t})}`);return e}const uo=L(),fo=async({device:e,options:t})=>{const o=te(t);await e.tap(o,ee(t)),process.stdout.write(`${JSON.stringify({tapped:!0},null,2)}
18
18
  `)},mo=async({device:e,options:t,positionals:[o]})=>{await e.type(o,wo(t)),process.stdout.write(`${JSON.stringify({typed:!0},null,2)}
19
- `)};function wo(e){const t=p(e.delay,"--delay");return t===void 0?void 0:{delay:t}}const z=Ze("appetize:cli");function ho(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");return Ve(e.controlSocket,{baseUrl:e.baseUrl,signal:t})}async function go(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");const o=await U(e.controlSocket,{signal:t}),{client:n}=De(o,{});return{close:()=>{o.close()},recordStart:async i=>await n.request("appetizeio.cli.record.start",i),recordStop:async()=>await n.request("appetizeio.cli.record.stop")}}function yo(e){return async(...t)=>{const{options:o,positionals:n}=Eo(t),i=o["session-id"],s=ke(typeof i=="string"?i:void 0),a=AbortSignal.any([J(),Le(s.pid)]);await e({options:o,positionals:n,record:s,signal:a})}}function So(e){return async({options:t,positionals:o,record:n,signal:i})=>{const s=await ho(n,i);try{await e({device:s,options:t,positionals:o,signal:i})}finally{s.close()}}}function vo(e){return async({options:t,positionals:o,record:n,signal:i})=>{const s=await go(n,i);try{await e({control:s,options:t,positionals:o,signal:i})}finally{s.close()}}}function xo(e){return typeof e=="object"&&e!==null}function Eo(e){const t=e.at(-1);return{options:xo(t)?t:{},positionals:e.slice(0,-1).filter(o=>typeof o=="string")}}const bo=["session-id",...We,...Qe,...ct,...wt,...Yt,...Rt,...no,...uo];function Oo(e){const t=Se("appetize").version(e);return g(t.command("session start <device-id> <target>").describe(["Start a device session and wait until it is ready","","Environment variables:","APPETIZE_API_TOKEN: Authorizes private APIs and session targets","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--device-os-version","Device OS version (e.g., 17 for iOS, 14 for Android)").option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--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").example("session start iphone15pro app_a1b2c3").example("session start pixel7 app_a1b2c3 --device-os-version 14"),jt),F(t.command("session stop").describe("Stop a running device session"),Mt),g(t.command("build list").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"),Ye),g(t.command("build upload <file>").describe([`Upload a build (${P.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(H)})`).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"'),ot),g(t.command("device list").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"),pt),f(t.command("press <button>").describe(`Press a device button: ${Z.join(", ")}; Android only: ${X.join(", ")}`).example("press home").example("press volumeUp"),vt),f(t.command("rotate <orientation>").describe("Rotate the device to an orientation: portrait, landscapeLeft, landscapeRight or upsideDown").example("rotate portrait").example("rotate landscapeLeft"),$t),f(t.command("screenshot [output]").describe("Take a screenshot of the current device screen").example("screenshot").example("screenshot home-screen").example("screenshot ./shots/checkout.png"),Pt),f(k(t.command("tap").describe(["Tap an element matched by a selector, or a screen position","",`Pick one of: ${S()}`])).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"),fo),f(k(k(t.command("swipe").describe(["Swipe in a direction, or along a path between two points","",`Pick a --direction with an optional --${l} starting point,`,`or both a --${l}-* and a --${u}-* point to form a path`,"",`Pick one per point: ${S({prefix:l})}`,` ${S({prefix:u})}`]).option("--direction",`Direction to travel: ${I.join(", ")}`).option("--distance","Travel as a fraction of the screen (0-1)"),{prefix:l}),{prefix:u}).option("--duration","Milliseconds the gesture travels for").option("--timeout",`Milliseconds to wait for any --${l}/--${u} 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"),io),f(Y(t.command("inspect [output]").describe("Capture the view hierarchy of the screen, or of an element matched by a selector")).option("--timeout","Milliseconds to wait for the element").option("--pretty","Pretty-print the JSON",!1).example("inspect").example("inspect --select-text 'Log in'").example("inspect hierarchy.json --pretty"),ht),f(t.command("type <text>").describe(`Type text; {...} presses a named key: ${_e.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"),mo),we(t.command("recording start <output>").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"),Et),we(t.command("recording stop").describe("Stop recording the device video and close the file").example("recording stop"),bt),g(t.command("skill install").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"),Qt),t}function g(e,t){return e.action(Io(t))}function F(e,t){return g(e.option("--session-id","Target session id; defaults to the sole active session"),yo(t))}function f(e,t){return F(e,So(t))}function we(e,t){return F(e,vo(t))}function he(e){return`${String(Date.now()-e)}ms`}async function $o(e,...t){const o=Date.now();z(`invoked: ${process.argv.slice(2).join(" ")}`);try{await e(...t),z(`ok in ${he(o)}`)}catch(n){const i=n instanceof Error?n.message:String(n);console.error(i),z(`failed after ${he(o)}:`,n),process.exitCode=1}}function Io(e){return(...t)=>{$o(e,...t)}}Re(),Oo("0.15.0").parse(process.argv,{string:bo});
19
+ `)};function wo(e){const t=p(e.delay,"--delay");return t===void 0?void 0:{delay:t}}const z=Ze("appetize:cli");function ho(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");return Ve(e.controlSocket,{baseUrl:e.baseUrl,signal:t})}async function go(e,t){if(e.controlSocket===void 0)throw new Error("Session is not ready");const o=await U(e.controlSocket,{signal:t}),{client:n}=De(o,{});return{close:()=>{o.close()},recordStart:async i=>await n.request("appetizeio.cli.record.start",i),recordStop:async()=>await n.request("appetizeio.cli.record.stop")}}function yo(e){return async(...t)=>{const{options:o,positionals:n}=Eo(t),i=o["session-id"],s=ke(typeof i=="string"?i:void 0),a=AbortSignal.any([J(),Le(s.pid)]);await e({options:o,positionals:n,record:s,signal:a})}}function So(e){return async({options:t,positionals:o,record:n,signal:i})=>{const s=await ho(n,i);try{await e({device:s,options:t,positionals:o,signal:i})}finally{s.close()}}}function vo(e){return async({options:t,positionals:o,record:n,signal:i})=>{const s=await go(n,i);try{await e({control:s,options:t,positionals:o,signal:i})}finally{s.close()}}}function xo(e){return typeof e=="object"&&e!==null}function Eo(e){const t=e.at(-1);return{options:xo(t)?t:{},positionals:e.slice(0,-1).filter(o=>typeof o=="string")}}const bo=["session-id",...We,...Qe,...ct,...wt,...Yt,...Rt,...no,...uo];function Oo(e){const t=Se("appetize").version(e);return g(t.command("session start <device-id> <target>").describe(["Start a device session and wait until it is ready","","Environment variables:","APPETIZE_API_TOKEN: Authorizes private APIs and session targets","APPETIZE_ENDPOINT: Appetize URL used when --endpoint is omitted"]).option("--device-os-version","Device OS version (e.g., 17 for iOS, 14 for Android)").option("--endpoint","Appetize URL (e.g., https://appetize.io); overrides APPETIZE_ENDPOINT").option("--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").example("session start iphone15pro app_a1b2c3").example("session start pixel7 app_a1b2c3 --device-os-version 14"),jt),F(t.command("session stop").describe("Stop a running device session"),Mt),g(t.command("build list").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"),Ye),g(t.command("build upload <file>").describe([`Upload a build (${P.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(H)})`).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"'),ot),g(t.command("device list").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"),pt),f(t.command("press <button>").describe(`Press a device button: ${Z.join(", ")}; Android only: ${X.join(", ")}`).example("press home").example("press volumeUp"),vt),f(t.command("rotate <orientation>").describe("Rotate the device to an orientation: portrait, landscapeLeft, landscapeRight or upsideDown").example("rotate portrait").example("rotate landscapeLeft"),$t),f(t.command("screenshot [output]").describe("Take a screenshot of the current device screen").example("screenshot").example("screenshot home-screen").example("screenshot ./shots/checkout.png"),Pt),f(k(t.command("tap").describe(["Tap an element matched by a selector, or a screen position","",`Pick one of: ${S()}`])).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"),fo),f(k(k(t.command("swipe").describe(["Swipe in a direction, or along a path between two points","",`Pick a --direction with an optional --${l} starting point,`,`or both a --${l}-* and a --${u}-* point to form a path`,"",`Pick one per point: ${S({prefix:l})}`,` ${S({prefix:u})}`]).option("--direction",`Direction to travel: ${I.join(", ")}`).option("--distance","Travel as a fraction of the screen (0-1)"),{prefix:l}),{prefix:u}).option("--duration","Milliseconds the gesture travels for").option("--timeout",`Milliseconds to wait for any --${l}/--${u} 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"),io),f(Y(t.command("inspect [output]").describe("Capture the view hierarchy of the screen, or of an element matched by a selector")).option("--timeout","Milliseconds to wait for the element").option("--pretty","Pretty-print the JSON",!1).example("inspect").example("inspect --select-text 'Log in'").example("inspect hierarchy.json --pretty"),ht),f(t.command("type <text>").describe(`Type text; {...} presses a named key: ${_e.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"),mo),we(t.command("recording start <output>").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"),Et),we(t.command("recording stop").describe("Stop recording the device video and close the file").example("recording stop"),bt),g(t.command("skill install").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"),Qt),t}function g(e,t){return e.action(Io(t))}function F(e,t){return g(e.option("--session-id","Target session id; defaults to the sole active session"),yo(t))}function f(e,t){return F(e,So(t))}function we(e,t){return F(e,vo(t))}function he(e){return`${String(Date.now()-e)}ms`}async function $o(e,...t){const o=Date.now();z(`invoked: ${process.argv.slice(2).join(" ")}`);try{await e(...t),z(`ok in ${he(o)}`)}catch(n){const i=n instanceof Error?n.message:String(n);console.error(i),z(`failed after ${he(o)}:`,n),process.exitCode=1}}function Io(e){return(...t)=>{$o(e,...t)}}Re(),Oo("0.15.2").parse(process.argv,{string:bo});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appetize/cli",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Run, inspect and control iOS and Android apps on Appetize cloud devices",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://appetize.io",
@@ -30,8 +30,8 @@
30
30
  "dependencies": {
31
31
  "debug": "^4.4.3",
32
32
  "human-id": "^4.2.1",
33
- "json-rpc-2.0": "^1.7.1",
34
- "mediabunny": "^1.55.3",
33
+ "json-rpc-2.0": "^1.8.0",
34
+ "mediabunny": "^1.55.5",
35
35
  "sade": "^1.8.1"
36
36
  }
37
37
  }
@@ -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.15.0'
13
+ cli-version: '0.15.2'
14
14
  ---
15
15
 
16
16
  # Appetize
@@ -208,6 +208,58 @@ exit if it never appears. That is your wait primitive: it returns the moment the
208
208
  element is there, and it tells you clearly when it is not. Sleeping guesses, and
209
209
  guesses are either slow or flaky.
210
210
 
211
+ ### 5. Change the code, reinstall, repeat
212
+
213
+ When the app itself is what you are changing, keep the session running and put
214
+ each new build on the device rather than starting over.
215
+
216
+ On Android, install the APK directly over adb:
217
+
218
+ ```bash
219
+ adb connect <adbSerial>
220
+ adb -s <adbSerial> install -r ./app/build/outputs/apk/debug/app-debug.apk
221
+ ```
222
+
223
+ This replaces the app in place and takes seconds. The `adbSerial` is the one
224
+ `session start` printed. Keep reading the app's logs from `logs.device`.
225
+
226
+ On iOS there is no direct install: a simulator build has to be produced by the
227
+ project's CI (a workflow that archives the `.app` and uploads it to Appetize,
228
+ often via `appetize build upload` or the Appetize upload action). Find that
229
+ workflow, push your change so it runs, then restart the session against the
230
+ build id it produced:
231
+
232
+ ```bash
233
+ appetize session stop
234
+ appetize session start iphone15pro <new-id>
235
+ ```
236
+
237
+ If you cannot tell from the repository's CI configuration how an iOS build
238
+ reaches Appetize, stop and ask how the change should be verified rather than
239
+ guessing.
240
+
241
+ Either way, go back to step 1 and inspect — do not assume the new build is
242
+ running until you have seen it.
243
+
244
+ ### 6. Show your work
245
+
246
+ When a step changes what is on screen, or reaches a state worth seeing, take a
247
+ screenshot and read the image into the transcript as you go. The person driving
248
+ sees what you see, rather than a description of it.
249
+
250
+ When a turn ends with a visual change or a final state that deserves a demo,
251
+ record the flow and surface the video the same way:
252
+
253
+ ```bash
254
+ appetize recording start demo
255
+ # drive the flow
256
+ appetize recording stop
257
+ ```
258
+
259
+ Where your environment lets you attach media to a pull request, include the
260
+ screenshots or video in the description as evidence of the change. Otherwise the
261
+ transcript is the record — never commit screenshots or recordings to the repo.
262
+
211
263
  ## Troubleshooting
212
264
 
213
265
  | What you see | What it means |