vibeview 0.26.0 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ApiClient,
4
+ AuthError,
5
+ NotFoundError,
4
6
  UsageError,
5
7
  createSpinner,
8
+ errorExitCode,
9
+ formatUtcTimestamp,
6
10
  loadConfig,
11
+ queueReasonCopy,
7
12
  resolveAuth,
8
13
  uploadAppBuild
9
- } from "./chunk-RFFCCPGZ.js";
14
+ } from "./chunk-FTTWLCTE.js";
10
15
 
11
16
  // src/lib/dev-config.ts
12
17
  import { readFileSync, writeFileSync } from "fs";
@@ -269,7 +274,7 @@ function formatBuildRowsHuman(app, rows) {
269
274
  r.version ? `${r.version}${r.build_number ? ` (${r.build_number})` : ""}` : r.build_number ?? "",
270
275
  r.platform,
271
276
  r.kind,
272
- r.uploaded_at ?? "",
277
+ formatUtcTimestamp(r.uploaded_at),
273
278
  (r.note ?? "").split("\n")[0]
274
279
  ]);
275
280
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...cells.map((c) => c[i].length)));
@@ -293,7 +298,7 @@ async function listBuilds(appId, opts, deps) {
293
298
  const message = err instanceof Error ? err.message : String(err);
294
299
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
295
300
  console.error(chalk.red(`error: ${message}`));
296
- process.exitCode = 1;
301
+ process.exitCode = errorExitCode(err);
297
302
  }
298
303
  }
299
304
 
@@ -1073,7 +1078,7 @@ async function buildCommand(opts, cloudDeps) {
1073
1078
  if (!(err instanceof BuildFailError)) {
1074
1079
  printFailure(opts, err instanceof Error ? err.message : String(err));
1075
1080
  }
1076
- process.exitCode = 1;
1081
+ process.exitCode = errorExitCode(err);
1077
1082
  }
1078
1083
  }
1079
1084
  function printFailure(opts, message, code) {
@@ -2182,6 +2187,11 @@ function parseWatchSeconds(flag) {
2182
2187
  }
2183
2188
  return seconds;
2184
2189
  }
2190
+ function queuedLine(position, reasonMessage) {
2191
+ const where = position ? ` (position ${position})` : "";
2192
+ const why = reasonMessage ?? "Every device is busy.";
2193
+ return `\u23F3 ${why} Waiting in the queue${where}, Ctrl-C to give up.`;
2194
+ }
2185
2195
  function emit(json, evt) {
2186
2196
  if (json) console.log(JSON.stringify(evt));
2187
2197
  }
@@ -2218,7 +2228,9 @@ async function startDevSession(cfg, onEvent = () => {
2218
2228
  event: "queued",
2219
2229
  session_id: sessionId,
2220
2230
  position: started.position ?? null,
2221
- queue_entry_id: queueEntryId
2231
+ queue_entry_id: queueEntryId,
2232
+ queue_reason: started.queue_reason ?? null,
2233
+ message: queueReasonCopy(started.queue_reason)
2222
2234
  });
2223
2235
  await pollWhileQueued(api, sessionId, QUEUE_WAIT_MS);
2224
2236
  queueEntryId = null;
@@ -2309,6 +2321,10 @@ function findBlockingWarning(info) {
2309
2321
  return (info.warnings ?? []).find((w) => BLOCKING_WARNING_PREFIXES.some((p) => w.includes(p)));
2310
2322
  }
2311
2323
  var QUEUE_WAIT_MS = 30 * 6e4;
2324
+ function sessionEndedMessage(info, when) {
2325
+ const reason = info.status === "failed" && info.failure_reason ? `: ${info.failure_reason}` : "";
2326
+ return `session ${info.status} ${when}${reason}`;
2327
+ }
2312
2328
  async function pollWhileQueued(api, sessionId, timeoutMs, intervalMs = 3e3) {
2313
2329
  const deadline = Date.now() + timeoutMs;
2314
2330
  for (; ; ) {
@@ -2316,7 +2332,7 @@ async function pollWhileQueued(api, sessionId, timeoutMs, intervalMs = 3e3) {
2316
2332
  const blocking = findBlockingWarning(info);
2317
2333
  if (blocking) throw new Error(blocking);
2318
2334
  if (info.status === "stopped" || info.status === "failed") {
2319
- throw new Error(`session ${info.status} while waiting for a device`);
2335
+ throw new Error(sessionEndedMessage(info, "while waiting for a device"));
2320
2336
  }
2321
2337
  if (info.status !== "pending" && info.status !== "queued") return info;
2322
2338
  if (Date.now() > deadline) {
@@ -2335,7 +2351,7 @@ async function pollUntilTunnelReady(api, sessionId, timeoutMs, intervalMs = 2e3)
2335
2351
  if (blocking) throw new Error(blocking);
2336
2352
  if (info.tunnel_url) return info;
2337
2353
  if (info.status === "stopped" || info.status === "failed") {
2338
- throw new Error(`session ${info.status} before the tunnel came up`);
2354
+ throw new Error(sessionEndedMessage(info, "before the tunnel came up"));
2339
2355
  }
2340
2356
  if (Date.now() > deadline) throw new Error("timed out waiting for the device to start");
2341
2357
  await new Promise((r) => setTimeout(r, intervalMs));
@@ -2354,7 +2370,7 @@ async function pollUntilActive(api, sessionId, timeoutMs, intervalMs = 2e3, onIn
2354
2370
  }
2355
2371
  if (info.status === "active" && !info.app_install_pending) return info;
2356
2372
  if (info.status === "stopped" || info.status === "failed") {
2357
- throw new Error(`session ${info.status} before it became active`);
2373
+ throw new Error(sessionEndedMessage(info, "before it became active"));
2358
2374
  }
2359
2375
  if (Date.now() > deadline) {
2360
2376
  throw new Error("timed out waiting for the session to become active");
@@ -2404,12 +2420,29 @@ function validateAppChoice(requested, fromFlag, apps) {
2404
2420
  if (apps.some((a) => a.public_id === requested)) return { appId: requested };
2405
2421
  return fromFlag ? { staleFlag: requested } : { staleCached: requested };
2406
2422
  }
2423
+ function staleAppFlagError(requested, platform, allApps) {
2424
+ const other = allApps.find((a) => a.public_id === requested);
2425
+ if (other) {
2426
+ return {
2427
+ message: `app "${requested}" is ${platformWithArticle(other.platform)} app, not ${platformWithArticle(platform)} app.`,
2428
+ hint: `Run \`vibeview dev --platform ${other.platform} --app ${requested}\`, or \`vibeview dev --platform ${platform}\` without --app to pick ${platformWithArticle(platform)} app.`
2429
+ };
2430
+ }
2431
+ return {
2432
+ message: `app "${requested}" not found \u2014 it may have been deleted.`,
2433
+ hint: "Run `vibeview dev` without --app to pick from the list."
2434
+ };
2435
+ }
2407
2436
  function buildAppStrategy(apps, buildFlag, json, isTTY) {
2408
2437
  if (buildFlag) return "build";
2409
2438
  if (apps.length === 0) return "build";
2410
2439
  if (json || !isTTY) return "pick";
2411
2440
  return "ask";
2412
2441
  }
2442
+ function noBuildConfigMessage(platform, interactive) {
2443
+ const addKey = `add platforms.${platform}.build {command, artifact} to vibeview.json`;
2444
+ return interactive ? `no build config for ${platform}: ${addKey}, or run \`vibeview build --platform ${platform}\` once to set it up` : `no build config for ${platform}: ${addKey}`;
2445
+ }
2413
2446
  function requireNonInteractive(json, message) {
2414
2447
  if (!json) return;
2415
2448
  emit(json, { event: "error", code: "config_required", message });
@@ -2477,11 +2510,9 @@ async function ensureConfig(api, opts) {
2477
2510
  const apps = appsForPlatform(allApps, platform);
2478
2511
  const choice = validateAppChoice(opts.app || entry?.appId, Boolean(opts.app), apps);
2479
2512
  if (choice.staleFlag) {
2480
- const message = `app "${choice.staleFlag}" not found \u2014 it may have been deleted.`;
2481
- console.error(
2482
- chalk4.red(`error: ${message}`) + `
2483
- Run \`vibeview dev\` without --app to pick from the list.`
2484
- );
2513
+ const { message, hint } = staleAppFlagError(choice.staleFlag, platform, allApps);
2514
+ console.error(chalk4.red(`error: ${message}`) + `
2515
+ ${hint}`);
2485
2516
  emit(opts.json, { event: "error", message });
2486
2517
  process.exit(2);
2487
2518
  }
@@ -2574,15 +2605,17 @@ Run \`vibeview dev\` without --app to pick from the list.`
2574
2605
  firstRunMetro,
2575
2606
  firstRunBuild
2576
2607
  });
2577
- saveDevConfig(process.cwd(), cfg);
2578
- if (isFirstRun) {
2579
- console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
2580
- const hint = fillLaterHint(
2581
- { build: !firstRunBuild, metro: PLATFORM_NEEDS_METRO[platform] && !firstRunMetro },
2582
- platform
2583
- );
2584
- if (hint) console.error(chalk4.dim(hint));
2585
- }
2608
+ const commitConfig = () => {
2609
+ saveDevConfig(process.cwd(), cfg);
2610
+ if (isFirstRun) {
2611
+ console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
2612
+ const hint = fillLaterHint(
2613
+ { build: !firstRunBuild, metro: PLATFORM_NEEDS_METRO[platform] && !firstRunMetro },
2614
+ platform
2615
+ );
2616
+ if (hint) console.error(chalk4.dim(hint));
2617
+ }
2618
+ };
2586
2619
  const finalMetro = isFirstRun ? resolveMetro(cfg, platform, opts.metroPort ? parseInt(opts.metroPort, 10) : void 0) : metroResolved;
2587
2620
  return {
2588
2621
  platform,
@@ -2592,7 +2625,9 @@ Run \`vibeview dev\` without --app to pick from the list.`
2592
2625
  metroPort: finalMetro.port,
2593
2626
  metroCommand: finalMetro.command,
2594
2627
  platformHasMetroOverride: finalMetro.platformHasOverride,
2595
- builtThisRun
2628
+ builtThisRun,
2629
+ savedConfig: cfg,
2630
+ commitConfig
2596
2631
  };
2597
2632
  }
2598
2633
  async function runDevInProcess(opts) {
@@ -2641,11 +2676,20 @@ async function runDevInProcess(opts) {
2641
2676
  process.exit(1);
2642
2677
  }
2643
2678
  }
2679
+ const interactive = !opts.json && Boolean(process.stdin.isTTY);
2680
+ const plannedBuild = cfg.savedConfig.platforms?.[cfg.platform]?.build;
2681
+ if (!plannedBuild && (opts.build === true && !cfg.builtThisRun || !PLATFORM_NEEDS_METRO[cfg.platform])) {
2682
+ const message = noBuildConfigMessage(cfg.platform, interactive);
2683
+ console.error(chalk4.red(`error: ${message}`));
2684
+ emit(opts.json, { event: "error", code: "config_required", message });
2685
+ process.exit(1);
2686
+ }
2687
+ cfg.commitConfig();
2644
2688
  if (opts.build === true && !cfg.builtThisRun) {
2645
2689
  const reloaded = loadDevConfig(process.cwd());
2646
2690
  const build = reloaded?.platforms?.[cfg.platform]?.build;
2647
2691
  if (!build) {
2648
- const message = `no build config for ${cfg.platform} \u2014 run vibeview build once to set it up`;
2692
+ const message = noBuildConfigMessage(cfg.platform, interactive);
2649
2693
  console.error(chalk4.red(`error: ${message}`));
2650
2694
  emit(opts.json, { event: "error", code: "config_required", message });
2651
2695
  process.exit(1);
@@ -2718,7 +2762,7 @@ async function runDevInProcess(opts) {
2718
2762
  rokuBuild = rokuEntry?.build;
2719
2763
  watchIgnore = rokuEntry?.watchIgnore ?? [];
2720
2764
  if (!rokuBuild) {
2721
- const message = `no build config for ${cfg.platform} \u2014 run \`vibeview build --platform ${cfg.platform}\` once to set it up`;
2765
+ const message = noBuildConfigMessage(cfg.platform, interactive);
2722
2766
  console.error(chalk4.red(`error: ${message}`));
2723
2767
  emit(opts.json, { event: "error", code: "config_required", message });
2724
2768
  process.exit(1);
@@ -2775,12 +2819,7 @@ async function runDevInProcess(opts) {
2775
2819
  console.error(chalk4.yellow(`\u26A0 ${evt.message}`));
2776
2820
  }
2777
2821
  } else if (evt.event === "queued") {
2778
- const where = evt.position ? ` (position ${evt.position})` : "";
2779
- console.error(
2780
- chalk4.yellow(
2781
- `\u23F3 every device is busy \u2014 waiting in the queue${where}, Ctrl-C to give up`
2782
- )
2783
- );
2822
+ console.error(chalk4.yellow(queuedLine(evt.position, evt.message)));
2784
2823
  } else if (evt.event === "tunnel_connected") {
2785
2824
  everConnected = true;
2786
2825
  console.error(chalk4.green("\u2713 tunnel connected \u2014 edit away, Fast Refresh is live"));
@@ -3025,7 +3064,7 @@ function formatDevicesHuman(devices) {
3025
3064
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
3026
3065
  const lines = [chalk5.bold(fmt(HEADERS2))];
3027
3066
  for (const row of rows.sort(rowSort)) {
3028
- const statusColored = row[5] === "available" ? chalk5.green(row[5]) : row[5] === "busy" ? chalk5.yellow(row[5]) : chalk5.dim(row[5]);
3067
+ const statusColored = row[5] === "available" ? chalk5.green(row[5]) : row[5] === "busy" || row[5] === "updating" ? chalk5.yellow(row[5]) : chalk5.dim(row[5]);
3029
3068
  lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
3030
3069
  }
3031
3070
  return lines.join("\n") + "\n";
@@ -3056,6 +3095,66 @@ function formatDeviceModelsHuman(models) {
3056
3095
  }
3057
3096
 
3058
3097
  // src/agent/registry.ts
3098
+ var PRESS_BUTTONS = {
3099
+ ios: ["home", "back", "lock", "siri", "enter"],
3100
+ android: ["home", "back", "lock", "enter"],
3101
+ appleTv: [
3102
+ "dpad_up",
3103
+ "dpad_down",
3104
+ "dpad_left",
3105
+ "dpad_right",
3106
+ "dpad_center",
3107
+ "back",
3108
+ "home",
3109
+ "menu"
3110
+ ],
3111
+ androidTv: [
3112
+ "dpad_up",
3113
+ "dpad_down",
3114
+ "dpad_left",
3115
+ "dpad_right",
3116
+ "dpad_center",
3117
+ "back",
3118
+ "home",
3119
+ "enter"
3120
+ ],
3121
+ roku: [
3122
+ "dpad_up",
3123
+ "dpad_down",
3124
+ "dpad_left",
3125
+ "dpad_right",
3126
+ "dpad_center",
3127
+ "back",
3128
+ "enter",
3129
+ "up",
3130
+ "down",
3131
+ "left",
3132
+ "right",
3133
+ "select",
3134
+ "backspace",
3135
+ "play",
3136
+ "rewind",
3137
+ "forward",
3138
+ "replay",
3139
+ "info"
3140
+ ]
3141
+ };
3142
+ var PRESS_PLATFORM_LABELS = [
3143
+ ["ios", "iPhone / iPad"],
3144
+ ["android", "Android phone / tablet"],
3145
+ ["appleTv", "Apple TV"],
3146
+ ["androidTv", "Android TV"],
3147
+ ["roku", "Roku (beta)"]
3148
+ ];
3149
+ var PRESS_BUTTONS_BY_PLATFORM = PRESS_PLATFORM_LABELS.map(([key, label]) => [label, PRESS_BUTTONS[key]]);
3150
+ function pressButtonLines(indent) {
3151
+ return PRESS_BUTTONS_BY_PLATFORM.map(
3152
+ ([platform, buttons]) => `${indent}${platform}: ${buttons.join(", ")}`
3153
+ );
3154
+ }
3155
+ function pressButtonsHelp() {
3156
+ return ["Valid buttons:", ...pressButtonLines(" ")].join("\n");
3157
+ }
3059
3158
  var AGENT_COMMANDS = [
3060
3159
  {
3061
3160
  verb: "ui-tree",
@@ -3089,7 +3188,7 @@ var AGENT_COMMANDS = [
3089
3188
  {
3090
3189
  verb: "screenshot",
3091
3190
  tool: "screenshot",
3092
- summary: "Capture a screenshot of the current screen. Use it to check how something LOOKS \u2014 the UI tree carries no colour, contrast, or stacking information, so a screen can pass every check the tree can express and still be visibly broken. Saved to disk; over MCP, pass inline: true to receive the image itself.",
3191
+ summary: "Capture a screenshot of the current screen. Use it to check how something LOOKS \u2014 the UI tree carries no colour, contrast, or stacking information, so a screen can pass every check the tree can express and still be visibly broken. Saved to disk; over MCP, pass inline: true to receive the image itself. On a phone or tablet its pixels are the coordinates `tap`/`drag` take, so a point read off it can be tapped as is.",
3093
3192
  mutating: false,
3094
3193
  params: [
3095
3194
  {
@@ -3324,7 +3423,7 @@ var AGENT_COMMANDS = [
3324
3423
  cli: "positional",
3325
3424
  type: "string",
3326
3425
  required: true,
3327
- description: "Text to type into the focused field."
3426
+ description: "Text to type into the focused field. On a phone or tablet a line break presses Enter, which submits the field (bash: $'query\\n'); `press enter` does the same."
3328
3427
  }
3329
3428
  ]
3330
3429
  },
@@ -3350,7 +3449,7 @@ var AGENT_COMMANDS = [
3350
3449
  cli: "positional",
3351
3450
  type: "string",
3352
3451
  required: true,
3353
- description: "Button to press: home, back, lock, siri, dpad_up, dpad_down, dpad_left, dpad_right, or dpad_center."
3452
+ description: `Button to press. ${pressButtonLines("").join(". ")}.`
3354
3453
  }
3355
3454
  ]
3356
3455
  },
@@ -3358,7 +3457,7 @@ var AGENT_COMMANDS = [
3358
3457
  // tools.py open_url: { url: string }.
3359
3458
  verb: "open-url",
3360
3459
  tool: "open_url",
3361
- summary: "Open a deep link or URL in the app under test.",
3460
+ summary: "Open a deep link or URL in the app under test. The link goes to that app only; one it has no screen for fails rather than opening a browser.",
3362
3461
  mutating: true,
3363
3462
  params: [
3364
3463
  {
@@ -3787,13 +3886,13 @@ async function runAgentVerb(cmd, positionals, opts) {
3787
3886
  } catch (err) {
3788
3887
  const message = err instanceof Error ? err.message : String(err);
3789
3888
  if (opts.json) {
3790
- const code = /^authentication failed/i.test(message) ? "auth_failed" : /^not found:/i.test(message) ? "not_found" : void 0;
3889
+ const code = err instanceof AuthError ? "auth_failed" : err instanceof NotFoundError ? "not_found" : void 0;
3791
3890
  console.log(JSON.stringify({ event: "error", code, message }));
3792
3891
  } else {
3793
3892
  process.stderr.write(`error: ${message}
3794
3893
  `);
3795
3894
  }
3796
- process.exit(1);
3895
+ process.exit(errorExitCode(err));
3797
3896
  }
3798
3897
  if (cmd.verb === "screenshot" && envelope.ok && envelope.result) {
3799
3898
  const outPath = opts.out || defaultScreenshotPath();
@@ -3819,6 +3918,7 @@ async function runAgentVerb(cmd, positionals, opts) {
3819
3918
  console.log(JSON.stringify(envelope));
3820
3919
  } else {
3821
3920
  console.log(renderEnvelope(envelope));
3921
+ if (cmd.verb === "press" && !envelope.ok) process.stderr.write(pressButtonsHelp() + "\n");
3822
3922
  }
3823
3923
  process.exit(agentExitCode(envelope));
3824
3924
  }
@@ -3854,6 +3954,7 @@ export {
3854
3954
  formatDevicesHuman,
3855
3955
  filterDeviceModels,
3856
3956
  formatDeviceModelsHuman,
3957
+ pressButtonsHelp,
3857
3958
  AGENT_COMMANDS,
3858
3959
  renderEnvelope,
3859
3960
  buildToolCall,
@@ -42,6 +42,51 @@ import { createWriteStream, mkdirSync as mkdirSync2, renameSync, rmSync } from "
42
42
  import { basename, join as join2, resolve, sep } from "path";
43
43
  import { pipeline } from "stream/promises";
44
44
  import { Readable } from "stream";
45
+
46
+ // src/lib/formatters/time.ts
47
+ function formatUtcTimestamp(iso) {
48
+ if (!iso) return "";
49
+ const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso);
50
+ const d = new Date(hasZone ? iso : `${iso}Z`);
51
+ if (Number.isNaN(d.getTime())) return iso;
52
+ return `${d.toISOString().slice(0, 19).replace("T", " ")} UTC`;
53
+ }
54
+ var NAIVE_ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
55
+ function withUtcTimestamps(value) {
56
+ if (typeof value === "string") {
57
+ return NAIVE_ISO_DATETIME.test(value) ? `${value}Z` : value;
58
+ }
59
+ if (Array.isArray(value)) return value.map((v) => withUtcTimestamps(v));
60
+ if (value !== null && typeof value === "object") {
61
+ const out = {};
62
+ for (const [k, v] of Object.entries(value)) {
63
+ out[k] = withUtcTimestamps(v);
64
+ }
65
+ return out;
66
+ }
67
+ return value;
68
+ }
69
+
70
+ // src/lib/api-client.ts
71
+ var AuthError = class extends Error {
72
+ constructor(message = "Authentication failed. Check your API token.") {
73
+ super(message);
74
+ this.name = "AuthError";
75
+ }
76
+ };
77
+ function errorExitCode(err, otherwise = 1) {
78
+ return err instanceof AuthError ? 2 : otherwise;
79
+ }
80
+ function reportCommandError(json, err) {
81
+ const message = err instanceof Error ? err.message : String(err);
82
+ if (json) printJsonError(err);
83
+ process.stderr.write(`error: ${message}
84
+ `);
85
+ }
86
+ function printJsonError(err) {
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ console.log(JSON.stringify({ event: "error", message }));
89
+ }
45
90
  var NotFoundError = class extends Error {
46
91
  constructor(message) {
47
92
  super(message);
@@ -52,13 +97,15 @@ function extractDetail(text) {
52
97
  try {
53
98
  const parsed = JSON.parse(text);
54
99
  if (parsed && typeof parsed.detail === "string") return parsed.detail;
100
+ if (parsed && parsed.detail && typeof parsed.detail.message === "string")
101
+ return parsed.detail.message;
55
102
  } catch {
56
103
  }
57
104
  return void 0;
58
105
  }
59
106
  async function parseJsonBody(resp) {
60
107
  if (resp.status === 204 || resp.status === 205) return void 0;
61
- return resp.json();
108
+ return withUtcTimestamps(await resp.json());
62
109
  }
63
110
  var ApiClient = class {
64
111
  constructor(baseUrl, token) {
@@ -96,7 +143,7 @@ var ApiClient = class {
96
143
  });
97
144
  if (!resp.ok) {
98
145
  const text = await resp.text().catch(() => "");
99
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
146
+ if (resp.status === 401) throw new AuthError();
100
147
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
101
148
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
102
149
  }
@@ -168,7 +215,7 @@ var ApiClient = class {
168
215
  });
169
216
  if (!resp.ok) {
170
217
  const text = await resp.text().catch(() => "");
171
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
218
+ if (resp.status === 401) throw new AuthError();
172
219
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
173
220
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
174
221
  }
@@ -188,7 +235,7 @@ var ApiClient = class {
188
235
  });
189
236
  if (!resp.ok) {
190
237
  const text = await resp.text().catch(() => "");
191
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
238
+ if (resp.status === 401) throw new AuthError();
192
239
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
193
240
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
194
241
  }
@@ -347,7 +394,7 @@ var ApiClient = class {
347
394
  const resp = await this.fetchOrExplain(url, { method: "GET", headers });
348
395
  if (!resp.ok) {
349
396
  const text = await resp.text().catch(() => "");
350
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
397
+ if (resp.status === 401) throw new AuthError();
351
398
  if (resp.status === 404)
352
399
  throw new NotFoundError(`Not found: ${extractDetail(text) ?? `/builds/${id}/artifact`}`);
353
400
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
@@ -646,6 +693,16 @@ var UsageError = class extends Error {
646
693
  this.name = "UsageError";
647
694
  }
648
695
  };
696
+ var DEVICE_STATUS_FILTERS = ["available", "busy", "updating"];
697
+ var QUEUE_REASON_COPY = {
698
+ devices_busy: "All devices are in use right now.",
699
+ devices_updating: "Devices are being updated and will be back shortly.",
700
+ plan_limit: "The most sessions this app can run at once are already running.",
701
+ link_limit: "The most people who can play at once are already playing."
702
+ };
703
+ function queueReasonCopy(reason) {
704
+ return reason ? QUEUE_REASON_COPY[reason] ?? null : null;
705
+ }
649
706
 
650
707
  // src/commands/upload-app.ts
651
708
  async function uploadAppBuild(filePath, options) {
@@ -729,6 +786,11 @@ export {
729
786
  loadConfig,
730
787
  saveConfig,
731
788
  resolveAuth,
789
+ formatUtcTimestamp,
790
+ AuthError,
791
+ errorExitCode,
792
+ reportCommandError,
793
+ printJsonError,
732
794
  NotFoundError,
733
795
  ApiClient,
734
796
  createSpinner,
@@ -736,6 +798,8 @@ export {
736
798
  EXIT_TEST_FAIL,
737
799
  EXIT_INFRA_ERROR,
738
800
  UsageError,
801
+ DEVICE_STATUS_FILTERS,
802
+ queueReasonCopy,
739
803
  uploadAppBuild,
740
804
  describeBuild,
741
805
  uploadApp
package/dist/cli.js CHANGED
@@ -16,29 +16,35 @@ import {
16
16
  loadDevConfig,
17
17
  newTransitionState,
18
18
  platformWithArticle,
19
+ pressButtonsHelp,
19
20
  printOutcomeLine,
20
21
  printTransition,
21
22
  readDevState,
22
23
  readMetroState,
23
24
  runAgentVerb
24
- } from "./chunk-JK3T3AMW.js";
25
+ } from "./chunk-7IOGMM5W.js";
25
26
  import {
26
27
  ApiClient,
28
+ DEVICE_STATUS_FILTERS,
27
29
  EXIT_INFRA_ERROR,
28
30
  EXIT_PASS,
29
31
  EXIT_TEST_FAIL,
30
32
  NotFoundError,
31
33
  UsageError,
32
34
  createSpinner,
35
+ errorExitCode,
36
+ formatUtcTimestamp,
33
37
  loadConfig,
38
+ printJsonError,
39
+ reportCommandError,
34
40
  resolveAuth,
35
41
  saveConfig,
36
42
  uploadApp
37
- } from "./chunk-RFFCCPGZ.js";
43
+ } from "./chunk-FTTWLCTE.js";
38
44
 
39
45
  // src/cli.ts
40
46
  import { createRequire } from "module";
41
- import { Command } from "commander";
47
+ import { Command, Option } from "commander";
42
48
 
43
49
  // src/commands/run-suite.ts
44
50
  import chalk3 from "chalk";
@@ -48,6 +54,26 @@ import chalk2 from "chalk";
48
54
 
49
55
  // src/lib/formatters/human.ts
50
56
  import chalk from "chalk";
57
+
58
+ // src/lib/formatters/cases.ts
59
+ function resultCases(result) {
60
+ if (Array.isArray(result.test_runs)) return result.test_runs;
61
+ const failedStep = result.steps?.find((s) => s.status !== "passed");
62
+ return [
63
+ {
64
+ case_name: result.case_name || result.run_id,
65
+ status: result.status,
66
+ duration_ms: result.duration_ms,
67
+ error_message: result.error_message ?? failedStep?.error ?? null,
68
+ error_kind: failedStep?.error_kind ?? null,
69
+ error_kind_description: failedStep?.error_kind_description ?? null,
70
+ screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
71
+ url: result.url
72
+ }
73
+ ];
74
+ }
75
+
76
+ // src/lib/formatters/human.ts
51
77
  function failureLines(failure) {
52
78
  const lines = [];
53
79
  if (failure.error_kind) {
@@ -73,15 +99,17 @@ function formatHuman(result, verbose) {
73
99
  } else {
74
100
  lines.push(` ${icon} ${chalk.bold(result.run_id)} ${chalk.dim(duration)}`);
75
101
  }
76
- const passed = result.test_runs.filter((t) => t.status === "passed").length;
77
- const total = result.test_runs.length;
102
+ const cases = resultCases(result);
103
+ const passed = cases.filter((t) => t.status === "passed").length;
104
+ const total = cases.length;
78
105
  lines.push(` ${chalk.dim(`${passed}/${total} cases passed`)}`);
79
106
  if (result.url) {
80
107
  lines.push(` ${chalk.dim(result.url)}`);
81
108
  }
82
- if (result.test_runs.length > 0) {
109
+ const suiteCases = result.test_runs ?? [];
110
+ if (suiteCases.length > 0) {
83
111
  lines.push("");
84
- for (const tr of result.test_runs) {
112
+ for (const tr of suiteCases) {
85
113
  const caseIcon = tr.status === "passed" ? chalk.green(" PASS") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
86
114
  const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
87
115
  lines.push(` ${caseIcon} ${tr.case_name} ${chalk.dim(caseDuration)}`);
@@ -130,10 +158,11 @@ function escapeXml(s) {
130
158
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
131
159
  }
132
160
  function formatJunit(result) {
133
- const suiteName = escapeXml(result.suite_name || result.run_id);
134
- const tests = result.test_runs.length;
135
- const failures = result.test_runs.filter((t) => t.status === "failed").length;
136
- const errors = result.test_runs.filter((t) => t.status === "error").length;
161
+ const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
162
+ const cases = resultCases(result);
163
+ const tests = cases.length;
164
+ const failures = cases.filter((t) => t.status === "failed").length;
165
+ const errors = cases.filter((t) => t.status === "error").length;
137
166
  const totalTime = result.duration_ms ? (result.duration_ms / 1e3).toFixed(1) : "0";
138
167
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
139
168
  const lines = [];
@@ -144,7 +173,7 @@ function formatJunit(result) {
144
173
  lines.push(
145
174
  ` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}" timestamp="${timestamp}">`
146
175
  );
147
- for (const tr of result.test_runs) {
176
+ for (const tr of cases) {
148
177
  const caseName = escapeXml(tr.case_name);
149
178
  const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
150
179
  if (tr.status === "passed") {
@@ -501,6 +530,7 @@ ${chalk3.dim("Run:")} ${response.run_id}
501
530
  process.exit(mapExitCode(result.status));
502
531
  } catch (err) {
503
532
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
533
+ if (options.output === "json") printJsonError(err);
504
534
  if (lastQueueEntryId !== void 0) {
505
535
  try {
506
536
  await client.cancelQueueEntry(lastQueueEntryId);
@@ -589,10 +619,13 @@ ${signal} received.
589
619
  const msg = err instanceof Error ? err.message : String(err);
590
620
  if (msg.startsWith("Not found:") || msg.includes("404") || /No suites match tags/.test(msg)) {
591
621
  const suffix = opts.platform ? ` on platform ${opts.platform}` : "";
592
- process.stderr.write(chalk4.yellow(`No suites match tags: ${opts.tag.join(", ")}${suffix}
622
+ const noMatch = `No suites match tags: ${opts.tag.join(", ")}${suffix}`;
623
+ if (opts.output === "json") printJsonError(noMatch);
624
+ process.stderr.write(chalk4.yellow(`${noMatch}
593
625
  `));
594
626
  process.exit(EXIT_INFRA_ERROR);
595
627
  }
628
+ if (opts.output === "json") printJsonError(err);
596
629
  process.stderr.write(chalk4.red(`Failed to start: ${msg}
597
630
  `));
598
631
  process.exit(EXIT_INFRA_ERROR);
@@ -676,6 +709,7 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
676
709
  process.stdin.pause();
677
710
  }
678
711
  if (!last) {
712
+ if (opts.output === "json") printJsonError("Timed out before any status was returned.");
679
713
  process.stderr.write(chalk4.red("Timed out before any status was returned.\n"));
680
714
  process.exit(EXIT_INFRA_ERROR);
681
715
  }
@@ -813,6 +847,7 @@ ${chalk5.dim("Run:")} ${response.run_id}
813
847
  process.exit(mapExitCode(result.status));
814
848
  } catch (err) {
815
849
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
850
+ if (options.output === "json") printJsonError(err);
816
851
  if (lastQueueEntryId !== void 0) {
817
852
  try {
818
853
  await client.cancelQueueEntry(lastQueueEntryId);
@@ -834,10 +869,16 @@ import chalk6 from "chalk";
834
869
  async function listSuites(options) {
835
870
  const { token, baseUrl } = resolveAuth(options);
836
871
  const client = new ApiClient(baseUrl, token);
837
- const suites = await client.listSuites({
838
- tags: options.tag && options.tag.length > 0 ? options.tag : void 0,
839
- match: options.match
840
- });
872
+ let suites;
873
+ try {
874
+ suites = await client.listSuites({
875
+ tags: options.tag && options.tag.length > 0 ? options.tag : void 0,
876
+ match: options.match
877
+ });
878
+ } catch (err) {
879
+ reportCommandError(options.output === "json", err);
880
+ process.exit(EXIT_INFRA_ERROR);
881
+ }
841
882
  if (options.output === "json") {
842
883
  console.log(JSON.stringify(suites, null, 2));
843
884
  return;
@@ -862,7 +903,13 @@ import chalk7 from "chalk";
862
903
  async function listApps(options) {
863
904
  const { token, baseUrl } = resolveAuth(options);
864
905
  const client = new ApiClient(baseUrl, token);
865
- const apps = await client.listApps();
906
+ let apps;
907
+ try {
908
+ apps = await client.listApps();
909
+ } catch (err) {
910
+ reportCommandError(options.output === "json", err);
911
+ process.exit(EXIT_INFRA_ERROR);
912
+ }
866
913
  if (options.output === "json") {
867
914
  console.log(JSON.stringify(apps, null, 2));
868
915
  return;
@@ -896,7 +943,7 @@ async function stopCommand(sessionId, opts, deps) {
896
943
  const message = err instanceof Error ? err.message : String(err);
897
944
  if (opts.json) console.log(JSON.stringify({ stopped: false, session_id: id, message }));
898
945
  console.error(chalk8.red(`error: ${message}`));
899
- process.exitCode = 1;
946
+ process.exitCode = errorExitCode(err);
900
947
  return;
901
948
  }
902
949
  const state = readDevState(process.cwd());
@@ -1278,8 +1325,10 @@ import chalk13 from "chalk";
1278
1325
  // src/lib/formatters/builds.ts
1279
1326
  import chalk12 from "chalk";
1280
1327
  var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
1281
- function formatCostCell(costMicro) {
1282
- return costMicro > 0 ? `$${(costMicro / 1e6).toFixed(2)}` : "";
1328
+ var UNBILLED_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
1329
+ function formatCostCell(costMicro, status) {
1330
+ if (costMicro <= 0 && UNBILLED_STATUSES.has(status)) return "-";
1331
+ return `$${(Math.max(costMicro, 0) / 1e6).toFixed(2)}`;
1283
1332
  }
1284
1333
  var STATUS_COLOR = {
1285
1334
  succeeded: chalk12.green,
@@ -1298,8 +1347,8 @@ function formatBuildsHuman(builds2) {
1298
1347
  b.id,
1299
1348
  b.platform,
1300
1349
  b.status,
1301
- b.created_at ?? "",
1302
- formatCostCell(b.cost_micro),
1350
+ formatUtcTimestamp(b.created_at),
1351
+ formatCostCell(b.cost_micro, b.status),
1303
1352
  b.app_public_id ?? ""
1304
1353
  ]);
1305
1354
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
@@ -1350,7 +1399,7 @@ async function buildsList(opts, deps) {
1350
1399
  process.exitCode = 0;
1351
1400
  } catch (err) {
1352
1401
  printError(opts, err);
1353
- process.exitCode = 1;
1402
+ process.exitCode = errorExitCode(err);
1354
1403
  }
1355
1404
  }
1356
1405
  async function buildsLogs(id, opts, deps) {
@@ -1373,7 +1422,7 @@ async function buildsLogs(id, opts, deps) {
1373
1422
  process.exitCode = final.status === "succeeded" ? 0 : 1;
1374
1423
  } catch (err) {
1375
1424
  printError(opts, err);
1376
- process.exitCode = 1;
1425
+ process.exitCode = errorExitCode(err);
1377
1426
  }
1378
1427
  }
1379
1428
  var MAX_CONSECUTIVE_POLL_FAILURES = 5;
@@ -1466,7 +1515,7 @@ async function buildsDownload(id, opts, deps) {
1466
1515
  process.exitCode = 0;
1467
1516
  } catch (err) {
1468
1517
  printError(opts, err);
1469
- process.exitCode = 1;
1518
+ process.exitCode = errorExitCode(err);
1470
1519
  }
1471
1520
  }
1472
1521
  async function buildsCancel(id, opts, deps) {
@@ -1483,7 +1532,7 @@ async function buildsCancel(id, opts, deps) {
1483
1532
  process.exitCode = 0;
1484
1533
  } catch (err) {
1485
1534
  printError(opts, err);
1486
- process.exitCode = 1;
1535
+ process.exitCode = errorExitCode(err);
1487
1536
  }
1488
1537
  }
1489
1538
  async function buildsDeleteArtifact(id, opts, deps) {
@@ -1500,7 +1549,7 @@ async function buildsDeleteArtifact(id, opts, deps) {
1500
1549
  process.exitCode = 0;
1501
1550
  } catch (err) {
1502
1551
  printError(opts, err);
1503
- process.exitCode = 1;
1552
+ process.exitCode = errorExitCode(err);
1504
1553
  }
1505
1554
  }
1506
1555
  function printError(opts, err) {
@@ -1692,7 +1741,7 @@ async function submitCommand(opts, deps) {
1692
1741
  process.exitCode = final.status === "succeeded" ? 0 : 1;
1693
1742
  } catch (err) {
1694
1743
  printError2(opts, err);
1695
- process.exitCode = 1;
1744
+ process.exitCode = errorExitCode(err);
1696
1745
  }
1697
1746
  }
1698
1747
  async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
@@ -1723,7 +1772,7 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
1723
1772
  process.exitCode = result.ready ? 0 : 1;
1724
1773
  } catch (err) {
1725
1774
  printError2(opts, err);
1726
- process.exitCode = 1;
1775
+ process.exitCode = errorExitCode(err);
1727
1776
  }
1728
1777
  }
1729
1778
  async function resolveLatestBuild(platform, deps) {
@@ -1853,7 +1902,7 @@ async function versionsList(opts, deps) {
1853
1902
  process.exitCode = 0;
1854
1903
  } catch (err) {
1855
1904
  printError3(opts, err);
1856
- process.exitCode = 1;
1905
+ process.exitCode = errorExitCode(err);
1857
1906
  }
1858
1907
  }
1859
1908
  async function versionsSet(buildNumber, opts, deps) {
@@ -1886,7 +1935,7 @@ async function versionsSet(buildNumber, opts, deps) {
1886
1935
  process.exitCode = 0;
1887
1936
  } catch (err) {
1888
1937
  printError3(opts, err);
1889
- process.exitCode = 1;
1938
+ process.exitCode = errorExitCode(err);
1890
1939
  }
1891
1940
  }
1892
1941
  var HEADERS2 = ["PLATFORM", "BUILD NUMBER", "UPDATED"];
@@ -1962,7 +2011,7 @@ Create an admin API token in the dashboard and use ${chalk16.bold("--token")} or
1962
2011
  console.log(JSON.stringify({ event: "error", message }));
1963
2012
  }
1964
2013
  console.error(chalk16.red(`error: ${message}`));
1965
- process.exitCode = 1;
2014
+ process.exitCode = errorExitCode(err);
1966
2015
  }
1967
2016
  }
1968
2017
 
@@ -2040,7 +2089,7 @@ async function envList(opts, deps) {
2040
2089
  process.exitCode = 0;
2041
2090
  } catch (err) {
2042
2091
  printError4(opts, err);
2043
- process.exitCode = 1;
2092
+ process.exitCode = errorExitCode(err);
2044
2093
  }
2045
2094
  }
2046
2095
  function formatEnvVarsHuman(variables) {
@@ -2089,7 +2138,7 @@ async function envSet(name, value, opts, deps) {
2089
2138
  process.exitCode = 0;
2090
2139
  } catch (err) {
2091
2140
  printError4(opts, err);
2092
- process.exitCode = 1;
2141
+ process.exitCode = errorExitCode(err);
2093
2142
  }
2094
2143
  }
2095
2144
  function resolveVisibility(opts) {
@@ -2122,7 +2171,7 @@ async function envRm(name, opts, deps) {
2122
2171
  process.exitCode = 0;
2123
2172
  } catch (err) {
2124
2173
  printError4(opts, err);
2125
- process.exitCode = 1;
2174
+ process.exitCode = errorExitCode(err);
2126
2175
  }
2127
2176
  }
2128
2177
  async function envPull(opts, deps) {
@@ -2154,7 +2203,7 @@ async function envPull(opts, deps) {
2154
2203
  process.exitCode = 0;
2155
2204
  } catch (err) {
2156
2205
  printError4(opts, err);
2157
- process.exitCode = 1;
2206
+ process.exitCode = errorExitCode(err);
2158
2207
  }
2159
2208
  }
2160
2209
  function isLossyEnvValue(value) {
@@ -2347,7 +2396,7 @@ async function credentialsAdd(opts, deps) {
2347
2396
  process.exitCode = 0;
2348
2397
  } catch (err) {
2349
2398
  printError5(opts, err);
2350
- process.exitCode = 1;
2399
+ process.exitCode = errorExitCode(err);
2351
2400
  }
2352
2401
  }
2353
2402
  async function credentialsGenerate(opts, deps) {
@@ -2421,7 +2470,7 @@ async function credentialsGenerate(opts, deps) {
2421
2470
  process.exitCode = 0;
2422
2471
  } catch (err) {
2423
2472
  printError5(opts, err);
2424
- process.exitCode = 1;
2473
+ process.exitCode = errorExitCode(err);
2425
2474
  }
2426
2475
  }
2427
2476
  async function generateAppleCredential(platform, opts, d) {
@@ -2457,7 +2506,7 @@ async function generateAppleCredential(platform, opts, d) {
2457
2506
  process.exitCode = 0;
2458
2507
  } catch (err) {
2459
2508
  printError5(opts, err);
2460
- process.exitCode = 1;
2509
+ process.exitCode = errorExitCode(err);
2461
2510
  }
2462
2511
  }
2463
2512
  async function credentialsDownload(name, opts, deps) {
@@ -2532,7 +2581,7 @@ async function credentialsDownload(name, opts, deps) {
2532
2581
  process.exitCode = 0;
2533
2582
  } catch (err) {
2534
2583
  printError5(opts, err);
2535
- process.exitCode = 1;
2584
+ process.exitCode = errorExitCode(err);
2536
2585
  }
2537
2586
  }
2538
2587
  async function ascKeyAdd(opts, deps) {
@@ -2572,7 +2621,7 @@ async function ascKeyAdd(opts, deps) {
2572
2621
  process.exitCode = 0;
2573
2622
  } catch (err) {
2574
2623
  printError5(opts, err);
2575
- process.exitCode = 1;
2624
+ process.exitCode = errorExitCode(err);
2576
2625
  }
2577
2626
  }
2578
2627
  async function ascKeyList(opts, deps) {
@@ -2587,7 +2636,7 @@ async function ascKeyList(opts, deps) {
2587
2636
  process.exitCode = 0;
2588
2637
  } catch (err) {
2589
2638
  printError5(opts, err);
2590
- process.exitCode = 1;
2639
+ process.exitCode = errorExitCode(err);
2591
2640
  }
2592
2641
  }
2593
2642
  function formatAscKeysHuman(keys) {
@@ -2595,7 +2644,7 @@ function formatAscKeysHuman(keys) {
2595
2644
  return "No App Store Connect keys found.\n";
2596
2645
  }
2597
2646
  const HEADERS3 = ["NAME", "KEY ID", "ISSUER ID", "CREATED"];
2598
- const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, k.created_at ?? ""]);
2647
+ const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, formatUtcTimestamp(k.created_at)]);
2599
2648
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2600
2649
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2601
2650
  const join3 = (cells) => cells.join(" ").trimEnd();
@@ -2615,7 +2664,7 @@ async function ascKeyRm(name, opts, deps) {
2615
2664
  process.exitCode = 0;
2616
2665
  } catch (err) {
2617
2666
  printError5(opts, err);
2618
- process.exitCode = 1;
2667
+ process.exitCode = errorExitCode(err);
2619
2668
  }
2620
2669
  }
2621
2670
  async function playKeyAdd(opts, deps) {
@@ -2648,7 +2697,7 @@ async function playKeyAdd(opts, deps) {
2648
2697
  process.exitCode = 0;
2649
2698
  } catch (err) {
2650
2699
  printError5(opts, err);
2651
- process.exitCode = 1;
2700
+ process.exitCode = errorExitCode(err);
2652
2701
  }
2653
2702
  }
2654
2703
  async function playKeyList(opts, deps) {
@@ -2663,7 +2712,7 @@ async function playKeyList(opts, deps) {
2663
2712
  process.exitCode = 0;
2664
2713
  } catch (err) {
2665
2714
  printError5(opts, err);
2666
- process.exitCode = 1;
2715
+ process.exitCode = errorExitCode(err);
2667
2716
  }
2668
2717
  }
2669
2718
  function formatPlayKeysHuman(keys) {
@@ -2671,7 +2720,7 @@ function formatPlayKeysHuman(keys) {
2671
2720
  return "No Google Play keys found.\n";
2672
2721
  }
2673
2722
  const HEADERS3 = ["NAME", "CLIENT EMAIL", "CREATED"];
2674
- const rows = keys.map((k) => [k.name, k.client_email, k.created_at ?? ""]);
2723
+ const rows = keys.map((k) => [k.name, k.client_email, formatUtcTimestamp(k.created_at)]);
2675
2724
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2676
2725
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2677
2726
  const join3 = (cells) => cells.join(" ").trimEnd();
@@ -2691,7 +2740,7 @@ async function playKeyRm(name, opts, deps) {
2691
2740
  process.exitCode = 0;
2692
2741
  } catch (err) {
2693
2742
  printError5(opts, err);
2694
- process.exitCode = 1;
2743
+ process.exitCode = errorExitCode(err);
2695
2744
  }
2696
2745
  }
2697
2746
  async function credentialsList(opts, deps) {
@@ -2712,7 +2761,7 @@ async function credentialsList(opts, deps) {
2712
2761
  process.exitCode = 0;
2713
2762
  } catch (err) {
2714
2763
  printError5(opts, err);
2715
- process.exitCode = 1;
2764
+ process.exitCode = errorExitCode(err);
2716
2765
  }
2717
2766
  }
2718
2767
  function formatIdentity(c) {
@@ -2731,10 +2780,10 @@ function earliestExpiry(c) {
2731
2780
  function formatExpires(c) {
2732
2781
  const parts = [];
2733
2782
  if (c.profile_expires_at) {
2734
- if (c.expires_at) parts.push(`cert ${c.expires_at}`);
2735
- parts.push(`profile ${c.profile_expires_at}`);
2783
+ if (c.expires_at) parts.push(`cert ${formatUtcTimestamp(c.expires_at)}`);
2784
+ parts.push(`profile ${formatUtcTimestamp(c.profile_expires_at)}`);
2736
2785
  } else if (c.expires_at) {
2737
- parts.push(c.expires_at);
2786
+ parts.push(formatUtcTimestamp(c.expires_at));
2738
2787
  }
2739
2788
  if (parts.length === 0) return "";
2740
2789
  const cell = parts.join(" \xB7 ");
@@ -2771,7 +2820,7 @@ async function credentialsRm(name, opts, deps) {
2771
2820
  process.exitCode = 0;
2772
2821
  } catch (err) {
2773
2822
  printError5(opts, err);
2774
- process.exitCode = 1;
2823
+ process.exitCode = errorExitCode(err);
2775
2824
  }
2776
2825
  }
2777
2826
  function printKeystoreWriteFailure(opts, err, result) {
@@ -2849,14 +2898,14 @@ async function webhooksAdd(opts, deps) {
2849
2898
  process.exitCode = 0;
2850
2899
  } catch (err) {
2851
2900
  printError6(opts, err);
2852
- process.exitCode = 1;
2901
+ process.exitCode = errorExitCode(err);
2853
2902
  }
2854
2903
  }
2855
2904
  function formatLastDelivery(ld) {
2856
2905
  if (!ld) return "\u2014";
2857
2906
  if (ld.status === "pending") return "pending";
2858
2907
  const code = ld.response_code != null ? ` (${ld.response_code})` : "";
2859
- const at = ld.at != null ? ` ${ld.at}` : "";
2908
+ const at = ld.at != null ? ` ${formatUtcTimestamp(ld.at)}` : "";
2860
2909
  return `${ld.status}${code}${at}`;
2861
2910
  }
2862
2911
  function formatWebhooksHuman(webhooks2) {
@@ -2869,7 +2918,7 @@ function formatWebhooksHuman(webhooks2) {
2869
2918
  w.url,
2870
2919
  w.active ? "yes" : "no",
2871
2920
  formatLastDelivery(w.last_delivery),
2872
- w.created_at
2921
+ formatUtcTimestamp(w.created_at)
2873
2922
  ]);
2874
2923
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2875
2924
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
@@ -2890,7 +2939,7 @@ async function webhooksList(opts, deps) {
2890
2939
  process.exitCode = 0;
2891
2940
  } catch (err) {
2892
2941
  printError6(opts, err);
2893
- process.exitCode = 1;
2942
+ process.exitCode = errorExitCode(err);
2894
2943
  }
2895
2944
  }
2896
2945
  async function webhooksRm(id, opts, deps) {
@@ -2905,7 +2954,7 @@ async function webhooksRm(id, opts, deps) {
2905
2954
  process.exitCode = 0;
2906
2955
  } catch (err) {
2907
2956
  printError6(opts, err);
2908
- process.exitCode = 1;
2957
+ process.exitCode = errorExitCode(err);
2909
2958
  }
2910
2959
  }
2911
2960
  function parseUtcTimestamp(iso) {
@@ -2939,7 +2988,7 @@ async function webhooksTest(id, opts, deps) {
2939
2988
  } else {
2940
2989
  if (!opts.json) {
2941
2990
  const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
2942
- console.error(chalk19.red(`\u2717 Failed (${code}) at ${ld.at}`));
2991
+ console.error(chalk19.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
2943
2992
  }
2944
2993
  process.exitCode = 1;
2945
2994
  }
@@ -2955,7 +3004,7 @@ async function webhooksTest(id, opts, deps) {
2955
3004
  process.exitCode = 1;
2956
3005
  } catch (err) {
2957
3006
  printError6(opts, err);
2958
- process.exitCode = 1;
3007
+ process.exitCode = errorExitCode(err);
2959
3008
  }
2960
3009
  }
2961
3010
  function printError6(opts, err) {
@@ -2978,8 +3027,7 @@ async function listDevicesCommand(options) {
2978
3027
  }
2979
3028
  process.exit(EXIT_PASS);
2980
3029
  } catch (err) {
2981
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
2982
- `);
3030
+ reportCommandError(options.output === "json", err);
2983
3031
  process.exit(EXIT_INFRA_ERROR);
2984
3032
  }
2985
3033
  return;
@@ -2997,8 +3045,7 @@ async function listDevicesCommand(options) {
2997
3045
  }
2998
3046
  process.exit(EXIT_PASS);
2999
3047
  } catch (err) {
3000
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
3001
- `);
3048
+ reportCommandError(options.output === "json", err);
3002
3049
  process.exit(EXIT_INFRA_ERROR);
3003
3050
  }
3004
3051
  }
@@ -3271,7 +3318,16 @@ program.command("list-apps").description("List apps in your organization").optio
3271
3318
  program.command("list-builds <app>").description(
3272
3319
  "List an app's uploaded builds, newest first: id, version, platform, debug/release, upload time and note (the ids `dev --build <id>` and `run-test --build` take)"
3273
3320
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the builds as a JSON array", false).action((app, opts) => listBuilds(app, opts));
3274
- program.command("list-devices").description("List devices registered with the platform (filterable by type and category)").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--type <type>", "Filter by device type: ios, android, roku").option("--category <category>", "Filter by category: phone, tablet, tv").option("--status <status>", "Filter by status: available, busy, offline").option(
3321
+ program.command("list-devices").description("List devices registered with the platform (filterable by type and category)").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").addOption(
3322
+ new Option("--type <type>", "Filter by device type").choices(["ios", "android", "roku"])
3323
+ ).addOption(
3324
+ new Option("--category <category>", "Filter by category").choices(["phone", "tablet", "tv"])
3325
+ ).addOption(
3326
+ new Option(
3327
+ "--status <status>",
3328
+ "Filter by status (updating = being updated, takes no new session yet)"
3329
+ ).choices([...DEVICE_STATUS_FILTERS])
3330
+ ).option(
3275
3331
  "--models",
3276
3332
  "List the device models you can start instead (the names `dev --model` takes)",
3277
3333
  false
@@ -3445,8 +3501,8 @@ program.command("dev-reload").description(
3445
3501
  program.command("mcp").description(
3446
3502
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
3447
3503
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
3448
- const { runMcpServer } = await import("./server-KYDTXPRZ.js");
3449
- await runMcpServer(opts);
3504
+ const { runMcpServer } = await import("./server-ZIUZ7U5T.js");
3505
+ await runMcpServer({ ...opts, version: pkg.version });
3450
3506
  });
3451
3507
  program.command("agent-setup").description(
3452
3508
  "Make VibeView discoverable to your coding agent: install the vibeview-agent skill (and optionally register the MCP server with Claude Code)"
@@ -3456,6 +3512,8 @@ for (const cmd of AGENT_COMMANDS) {
3456
3512
  const flagParams = cmd.params.filter((p) => p.cli !== "positional");
3457
3513
  const argSignature = positionalParams.map((p) => p.required ? `<${p.name}>` : `[${p.name}]`).join(" ");
3458
3514
  let command = program.command(argSignature ? `${cmd.verb} ${argSignature}` : cmd.verb).description(cmd.summary);
3515
+ if (cmd.verb === "press") command.addHelpText("after", `
3516
+ ${pressButtonsHelp()}`);
3459
3517
  for (const param of flagParams) {
3460
3518
  const flagArg = param.type === "number" ? `${param.cli} <n>` : `${param.cli} <value>`;
3461
3519
  command = command.option(flagArg, param.description);
@@ -21,11 +21,11 @@ import {
21
21
  readDevState,
22
22
  renderEnvelope,
23
23
  startDevSession
24
- } from "./chunk-JK3T3AMW.js";
24
+ } from "./chunk-7IOGMM5W.js";
25
25
  import {
26
26
  ApiClient,
27
27
  resolveAuth
28
- } from "./chunk-RFFCCPGZ.js";
28
+ } from "./chunk-FTTWLCTE.js";
29
29
 
30
30
  // src/mcp/server.ts
31
31
  import { writeFileSync } from "fs";
@@ -377,7 +377,7 @@ async function handleUploadApp(args, deps) {
377
377
  }
378
378
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
379
379
  const upload = deps.uploadApp ?? (async (f, n) => {
380
- const { uploadAppBuild } = await import("./upload-app-E32EZXYQ.js");
380
+ const { uploadAppBuild } = await import("./upload-app-MDKTSG4O.js");
381
381
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
382
382
  return result2;
383
383
  });
@@ -578,7 +578,7 @@ async function createMcpServer(opts) {
578
578
  agentTool: client.agentTool.bind(client),
579
579
  devSession
580
580
  };
581
- const server = new McpServer({ name: "vibeview", version: "1.0.0" });
581
+ const server = new McpServer({ name: "vibeview", version: opts.version ?? "0.0.0" });
582
582
  function zodShape(def) {
583
583
  const shape = {};
584
584
  for (const [key, prop] of Object.entries(def.properties)) {
@@ -3,7 +3,7 @@ import {
3
3
  describeBuild,
4
4
  uploadApp,
5
5
  uploadAppBuild
6
- } from "./chunk-RFFCCPGZ.js";
6
+ } from "./chunk-FTTWLCTE.js";
7
7
  export {
8
8
  describeBuild,
9
9
  uploadApp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "description": "VibeView CLI -- run mobile tests from CI/CD pipelines",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",
@@ -134,6 +134,9 @@ it rather than letting it appear unexplained in their `git status`.
134
134
  ```bash
135
135
  vibeview scroll-to "Delete account" --session <id>
136
136
  ```
137
+ A `scroll-to` that never finds its text, or a `type` whose field does not
138
+ end up holding the text, prints `error` and exits 1 — check the reason
139
+ line and the screen before retrying.
137
140
  - **Precise two-point gestures** (sliders, drag-and-drop reorder, map
138
141
  panning) need `drag` — `swipe`/`scroll` are direction-only and can't
139
142
  express them. Each end is independently a ref or `x,y`:
@@ -247,18 +250,18 @@ human-readable text.
247
250
  |------|------|---------|
248
251
  | `ui-tree` | — | Fetch the ref-annotated UI tree for the current screen. |
249
252
  | `logs` | `[--tail <n>] [--since <cursor>]` | Read recent app logs from the device (JS console, native errors, crash messages). The response's `cursor` feeds the next call's `--since`. |
250
- | `screenshot` | `[--out <path>]` | Capture a screenshot of the current screen and save it to disk. |
251
- | `tap` | `<target>` | Tap an element by ref (e.g. `@e5`), or raw coordinates (e.g. `100,200`). |
253
+ | `screenshot` | `[--out <path>]` | Capture a screenshot of the current screen and save it to disk. On a phone or tablet its pixels are tap coordinates: a point read off it can be tapped as is — never rescale. |
254
+ | `tap` | `<target>` | Tap an element by ref (e.g. `@e5`), or coordinates (e.g. `100,200`) in the space of the tree's rects and the screenshot's pixels. |
252
255
  | `long-press` | `<ref>` `[--ms <n>]` | Long-press (touch and hold) an element by its ref. |
253
256
  | `swipe` | `<direction>` `[--distance <short\|medium\|long>]` | Perform a scrollbar-semantic swipe gesture in one of four directions. |
254
257
  | `scroll` | `<direction>` | Scroll the current view up, down, left, or right using a gesture preset. |
255
258
  | `scroll-to` | `<text>` `[--direction <up\|down\|left\|right>] [--max-scrolls <n>] [--element-type <type>]` | Scroll until an element matching the text is visible. Stops at end of list. |
256
259
  | `drag` | `<from>` `<to>` `[--velocity <n>] [--press-ms <n>] [--hold-ms <n>]` | Drag between two points, each an `@ref` or `x,y`. The only precise two-point gesture. |
257
260
  | `alert` | `<get\|accept\|dismiss>` `[--button <label>]` | Inspect or respond to a system alert on iOS or Apple TV. |
258
- | `type` | `<text>` | Type text into the currently focused input field. |
261
+ | `type` | `<text>` | Type text into the currently focused input field. On a phone or tablet a line break presses Enter and submits the field (`$'query\n'`), like `press enter`. |
259
262
  | `clear-text` | — | Clear the text in the currently focused input field. |
260
263
  | `press` | `<button>` | Press a device button or perform a system gesture (home, back, d-pad, etc). |
261
- | `open-url` | `<url>` | Open a deep link or URL in the app under test. |
264
+ | `open-url` | `<url>` | Open a deep link or URL in the app under test. The link goes to that app only; a link it has no screen for fails (no browser). |
262
265
  | `set-posture` | `<closed\|partial\|open>` or `--angle <0-180>` | Fold or unfold a foldable device (form factor `foldable`: the iPhone Duo, Android foldables) to a preset or an exact hinge angle — exactly one. Reports posture, angle and lit screen. Errors on a device without a hinge. |
263
266
  | `rotate` | `[--degrees <90\|180\|270>]` | Rotate the device and report the new orientation. Phones/tablets and Android foldables toggle portrait/landscape (90 only); the iPhone Duo turns a quarter clockwise by default. Not on TV. |
264
267
  | `wait` | `[--ms <n>]` | Wait for a specified number of seconds before continuing (default: 2s). |