vibeview 0.26.0 → 0.27.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.
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ApiClient,
4
+ AuthError,
5
+ NotFoundError,
4
6
  UsageError,
5
7
  createSpinner,
8
+ errorExitCode,
6
9
  loadConfig,
7
10
  resolveAuth,
8
11
  uploadAppBuild
9
- } from "./chunk-RFFCCPGZ.js";
12
+ } from "./chunk-U7FCGMWG.js";
10
13
 
11
14
  // src/lib/dev-config.ts
12
15
  import { readFileSync, writeFileSync } from "fs";
@@ -246,6 +249,17 @@ function checkDevBuild(app, buildId, platform) {
246
249
 
247
250
  // src/commands/list-builds.ts
248
251
  import chalk from "chalk";
252
+
253
+ // src/lib/formatters/time.ts
254
+ function formatUtcTimestamp(iso) {
255
+ if (!iso) return "";
256
+ const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso);
257
+ const d = new Date(hasZone ? iso : `${iso}Z`);
258
+ if (Number.isNaN(d.getTime())) return iso;
259
+ return `${d.toISOString().slice(0, 19).replace("T", " ")} UTC`;
260
+ }
261
+
262
+ // src/commands/list-builds.ts
249
263
  function buildRows(app) {
250
264
  return app.builds.map((b) => ({
251
265
  id: b.id,
@@ -269,7 +283,7 @@ function formatBuildRowsHuman(app, rows) {
269
283
  r.version ? `${r.version}${r.build_number ? ` (${r.build_number})` : ""}` : r.build_number ?? "",
270
284
  r.platform,
271
285
  r.kind,
272
- r.uploaded_at ?? "",
286
+ formatUtcTimestamp(r.uploaded_at),
273
287
  (r.note ?? "").split("\n")[0]
274
288
  ]);
275
289
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...cells.map((c) => c[i].length)));
@@ -293,7 +307,7 @@ async function listBuilds(appId, opts, deps) {
293
307
  const message = err instanceof Error ? err.message : String(err);
294
308
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
295
309
  console.error(chalk.red(`error: ${message}`));
296
- process.exitCode = 1;
310
+ process.exitCode = errorExitCode(err);
297
311
  }
298
312
  }
299
313
 
@@ -1073,7 +1087,7 @@ async function buildCommand(opts, cloudDeps) {
1073
1087
  if (!(err instanceof BuildFailError)) {
1074
1088
  printFailure(opts, err instanceof Error ? err.message : String(err));
1075
1089
  }
1076
- process.exitCode = 1;
1090
+ process.exitCode = errorExitCode(err);
1077
1091
  }
1078
1092
  }
1079
1093
  function printFailure(opts, message, code) {
@@ -2410,6 +2424,10 @@ function buildAppStrategy(apps, buildFlag, json, isTTY) {
2410
2424
  if (json || !isTTY) return "pick";
2411
2425
  return "ask";
2412
2426
  }
2427
+ function noBuildConfigMessage(platform, interactive) {
2428
+ const addKey = `add platforms.${platform}.build {command, artifact} to vibeview.json`;
2429
+ 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}`;
2430
+ }
2413
2431
  function requireNonInteractive(json, message) {
2414
2432
  if (!json) return;
2415
2433
  emit(json, { event: "error", code: "config_required", message });
@@ -2574,15 +2592,17 @@ Run \`vibeview dev\` without --app to pick from the list.`
2574
2592
  firstRunMetro,
2575
2593
  firstRunBuild
2576
2594
  });
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
- }
2595
+ const commitConfig = () => {
2596
+ saveDevConfig(process.cwd(), cfg);
2597
+ if (isFirstRun) {
2598
+ console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
2599
+ const hint = fillLaterHint(
2600
+ { build: !firstRunBuild, metro: PLATFORM_NEEDS_METRO[platform] && !firstRunMetro },
2601
+ platform
2602
+ );
2603
+ if (hint) console.error(chalk4.dim(hint));
2604
+ }
2605
+ };
2586
2606
  const finalMetro = isFirstRun ? resolveMetro(cfg, platform, opts.metroPort ? parseInt(opts.metroPort, 10) : void 0) : metroResolved;
2587
2607
  return {
2588
2608
  platform,
@@ -2592,7 +2612,9 @@ Run \`vibeview dev\` without --app to pick from the list.`
2592
2612
  metroPort: finalMetro.port,
2593
2613
  metroCommand: finalMetro.command,
2594
2614
  platformHasMetroOverride: finalMetro.platformHasOverride,
2595
- builtThisRun
2615
+ builtThisRun,
2616
+ savedConfig: cfg,
2617
+ commitConfig
2596
2618
  };
2597
2619
  }
2598
2620
  async function runDevInProcess(opts) {
@@ -2641,11 +2663,20 @@ async function runDevInProcess(opts) {
2641
2663
  process.exit(1);
2642
2664
  }
2643
2665
  }
2666
+ const interactive = !opts.json && Boolean(process.stdin.isTTY);
2667
+ const plannedBuild = cfg.savedConfig.platforms?.[cfg.platform]?.build;
2668
+ if (!plannedBuild && (opts.build === true && !cfg.builtThisRun || !PLATFORM_NEEDS_METRO[cfg.platform])) {
2669
+ const message = noBuildConfigMessage(cfg.platform, interactive);
2670
+ console.error(chalk4.red(`error: ${message}`));
2671
+ emit(opts.json, { event: "error", code: "config_required", message });
2672
+ process.exit(1);
2673
+ }
2674
+ cfg.commitConfig();
2644
2675
  if (opts.build === true && !cfg.builtThisRun) {
2645
2676
  const reloaded = loadDevConfig(process.cwd());
2646
2677
  const build = reloaded?.platforms?.[cfg.platform]?.build;
2647
2678
  if (!build) {
2648
- const message = `no build config for ${cfg.platform} \u2014 run vibeview build once to set it up`;
2679
+ const message = noBuildConfigMessage(cfg.platform, interactive);
2649
2680
  console.error(chalk4.red(`error: ${message}`));
2650
2681
  emit(opts.json, { event: "error", code: "config_required", message });
2651
2682
  process.exit(1);
@@ -2718,7 +2749,7 @@ async function runDevInProcess(opts) {
2718
2749
  rokuBuild = rokuEntry?.build;
2719
2750
  watchIgnore = rokuEntry?.watchIgnore ?? [];
2720
2751
  if (!rokuBuild) {
2721
- const message = `no build config for ${cfg.platform} \u2014 run \`vibeview build --platform ${cfg.platform}\` once to set it up`;
2752
+ const message = noBuildConfigMessage(cfg.platform, interactive);
2722
2753
  console.error(chalk4.red(`error: ${message}`));
2723
2754
  emit(opts.json, { event: "error", code: "config_required", message });
2724
2755
  process.exit(1);
@@ -3056,6 +3087,26 @@ function formatDeviceModelsHuman(models) {
3056
3087
  }
3057
3088
 
3058
3089
  // src/agent/registry.ts
3090
+ var PRESS_BUTTONS_BY_PLATFORM = [
3091
+ ["iPhone / iPad", ["home", "back", "lock", "siri"]],
3092
+ ["Android phone / tablet", ["home", "back", "lock"]],
3093
+ [
3094
+ "Apple TV / Android TV",
3095
+ ["dpad_up", "dpad_down", "dpad_left", "dpad_right", "dpad_center", "back", "home"]
3096
+ ],
3097
+ [
3098
+ "Roku (beta)",
3099
+ ["up", "down", "left", "right", "select", "back", "(dpad_* also work; home is not forwarded)"]
3100
+ ]
3101
+ ];
3102
+ function pressButtonsHelp() {
3103
+ return [
3104
+ "Valid buttons:",
3105
+ ...PRESS_BUTTONS_BY_PLATFORM.map(
3106
+ ([platform, buttons]) => ` ${platform}: ${buttons.join(", ")}`
3107
+ )
3108
+ ].join("\n");
3109
+ }
3059
3110
  var AGENT_COMMANDS = [
3060
3111
  {
3061
3112
  verb: "ui-tree",
@@ -3089,7 +3140,7 @@ var AGENT_COMMANDS = [
3089
3140
  {
3090
3141
  verb: "screenshot",
3091
3142
  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.",
3143
+ 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
3144
  mutating: false,
3094
3145
  params: [
3095
3146
  {
@@ -3324,7 +3375,7 @@ var AGENT_COMMANDS = [
3324
3375
  cli: "positional",
3325
3376
  type: "string",
3326
3377
  required: true,
3327
- description: "Text to type into the focused field."
3378
+ 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
3379
  }
3329
3380
  ]
3330
3381
  },
@@ -3350,7 +3401,7 @@ var AGENT_COMMANDS = [
3350
3401
  cli: "positional",
3351
3402
  type: "string",
3352
3403
  required: true,
3353
- description: "Button to press: home, back, lock, siri, dpad_up, dpad_down, dpad_left, dpad_right, or dpad_center."
3404
+ description: "Button to press. iPhone/iPad: home, back, lock, siri. Android: home, back, lock. Apple TV / Android TV: dpad_up, dpad_down, dpad_left, dpad_right, dpad_center, back, home. Roku: up, down, left, right, select, back (dpad_* also work; home is not forwarded)."
3354
3405
  }
3355
3406
  ]
3356
3407
  },
@@ -3358,7 +3409,7 @@ var AGENT_COMMANDS = [
3358
3409
  // tools.py open_url: { url: string }.
3359
3410
  verb: "open-url",
3360
3411
  tool: "open_url",
3361
- summary: "Open a deep link or URL in the app under test.",
3412
+ 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
3413
  mutating: true,
3363
3414
  params: [
3364
3415
  {
@@ -3787,13 +3838,13 @@ async function runAgentVerb(cmd, positionals, opts) {
3787
3838
  } catch (err) {
3788
3839
  const message = err instanceof Error ? err.message : String(err);
3789
3840
  if (opts.json) {
3790
- const code = /^authentication failed/i.test(message) ? "auth_failed" : /^not found:/i.test(message) ? "not_found" : void 0;
3841
+ const code = err instanceof AuthError ? "auth_failed" : err instanceof NotFoundError ? "not_found" : void 0;
3791
3842
  console.log(JSON.stringify({ event: "error", code, message }));
3792
3843
  } else {
3793
3844
  process.stderr.write(`error: ${message}
3794
3845
  `);
3795
3846
  }
3796
- process.exit(1);
3847
+ process.exit(errorExitCode(err));
3797
3848
  }
3798
3849
  if (cmd.verb === "screenshot" && envelope.ok && envelope.result) {
3799
3850
  const outPath = opts.out || defaultScreenshotPath();
@@ -3819,6 +3870,7 @@ async function runAgentVerb(cmd, positionals, opts) {
3819
3870
  console.log(JSON.stringify(envelope));
3820
3871
  } else {
3821
3872
  console.log(renderEnvelope(envelope));
3873
+ if (cmd.verb === "press" && !envelope.ok) process.stderr.write(pressButtonsHelp() + "\n");
3822
3874
  }
3823
3875
  process.exit(agentExitCode(envelope));
3824
3876
  }
@@ -3831,6 +3883,7 @@ export {
3831
3883
  loadDevConfig,
3832
3884
  parseBuildId,
3833
3885
  checkDevBuild,
3886
+ formatUtcTimestamp,
3834
3887
  buildRows,
3835
3888
  formatBuildRowsHuman,
3836
3889
  listBuilds,
@@ -3854,6 +3907,7 @@ export {
3854
3907
  formatDevicesHuman,
3855
3908
  filterDeviceModels,
3856
3909
  formatDeviceModelsHuman,
3910
+ pressButtonsHelp,
3857
3911
  AGENT_COMMANDS,
3858
3912
  renderEnvelope,
3859
3913
  buildToolCall,
@@ -42,6 +42,21 @@ 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
+ var AuthError = class extends Error {
46
+ constructor(message = "Authentication failed. Check your API token.") {
47
+ super(message);
48
+ this.name = "AuthError";
49
+ }
50
+ };
51
+ function errorExitCode(err, otherwise = 1) {
52
+ return err instanceof AuthError ? 2 : otherwise;
53
+ }
54
+ function reportCommandError(json, err) {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ if (json) console.log(JSON.stringify({ event: "error", message }));
57
+ process.stderr.write(`error: ${message}
58
+ `);
59
+ }
45
60
  var NotFoundError = class extends Error {
46
61
  constructor(message) {
47
62
  super(message);
@@ -52,6 +67,8 @@ function extractDetail(text) {
52
67
  try {
53
68
  const parsed = JSON.parse(text);
54
69
  if (parsed && typeof parsed.detail === "string") return parsed.detail;
70
+ if (parsed && parsed.detail && typeof parsed.detail.message === "string")
71
+ return parsed.detail.message;
55
72
  } catch {
56
73
  }
57
74
  return void 0;
@@ -96,7 +113,7 @@ var ApiClient = class {
96
113
  });
97
114
  if (!resp.ok) {
98
115
  const text = await resp.text().catch(() => "");
99
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
116
+ if (resp.status === 401) throw new AuthError();
100
117
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
101
118
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
102
119
  }
@@ -168,7 +185,7 @@ var ApiClient = class {
168
185
  });
169
186
  if (!resp.ok) {
170
187
  const text = await resp.text().catch(() => "");
171
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
188
+ if (resp.status === 401) throw new AuthError();
172
189
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
173
190
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
174
191
  }
@@ -188,7 +205,7 @@ var ApiClient = class {
188
205
  });
189
206
  if (!resp.ok) {
190
207
  const text = await resp.text().catch(() => "");
191
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
208
+ if (resp.status === 401) throw new AuthError();
192
209
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
193
210
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
194
211
  }
@@ -347,7 +364,7 @@ var ApiClient = class {
347
364
  const resp = await this.fetchOrExplain(url, { method: "GET", headers });
348
365
  if (!resp.ok) {
349
366
  const text = await resp.text().catch(() => "");
350
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
367
+ if (resp.status === 401) throw new AuthError();
351
368
  if (resp.status === 404)
352
369
  throw new NotFoundError(`Not found: ${extractDetail(text) ?? `/builds/${id}/artifact`}`);
353
370
  throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
@@ -729,6 +746,9 @@ export {
729
746
  loadConfig,
730
747
  saveConfig,
731
748
  resolveAuth,
749
+ AuthError,
750
+ errorExitCode,
751
+ reportCommandError,
732
752
  NotFoundError,
733
753
  ApiClient,
734
754
  createSpinner,
package/dist/cli.js CHANGED
@@ -11,17 +11,19 @@ import {
11
11
  formatDeviceModelsHuman,
12
12
  formatDevicesHuman,
13
13
  formatDuration,
14
+ formatUtcTimestamp,
14
15
  killOwnedMetro,
15
16
  listBuilds,
16
17
  loadDevConfig,
17
18
  newTransitionState,
18
19
  platformWithArticle,
20
+ pressButtonsHelp,
19
21
  printOutcomeLine,
20
22
  printTransition,
21
23
  readDevState,
22
24
  readMetroState,
23
25
  runAgentVerb
24
- } from "./chunk-JK3T3AMW.js";
26
+ } from "./chunk-46X7UN6Y.js";
25
27
  import {
26
28
  ApiClient,
27
29
  EXIT_INFRA_ERROR,
@@ -30,15 +32,17 @@ import {
30
32
  NotFoundError,
31
33
  UsageError,
32
34
  createSpinner,
35
+ errorExitCode,
33
36
  loadConfig,
37
+ reportCommandError,
34
38
  resolveAuth,
35
39
  saveConfig,
36
40
  uploadApp
37
- } from "./chunk-RFFCCPGZ.js";
41
+ } from "./chunk-U7FCGMWG.js";
38
42
 
39
43
  // src/cli.ts
40
44
  import { createRequire } from "module";
41
- import { Command } from "commander";
45
+ import { Command, Option } from "commander";
42
46
 
43
47
  // src/commands/run-suite.ts
44
48
  import chalk3 from "chalk";
@@ -834,10 +838,16 @@ import chalk6 from "chalk";
834
838
  async function listSuites(options) {
835
839
  const { token, baseUrl } = resolveAuth(options);
836
840
  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
- });
841
+ let suites;
842
+ try {
843
+ suites = await client.listSuites({
844
+ tags: options.tag && options.tag.length > 0 ? options.tag : void 0,
845
+ match: options.match
846
+ });
847
+ } catch (err) {
848
+ reportCommandError(options.output === "json", err);
849
+ process.exit(EXIT_INFRA_ERROR);
850
+ }
841
851
  if (options.output === "json") {
842
852
  console.log(JSON.stringify(suites, null, 2));
843
853
  return;
@@ -862,7 +872,13 @@ import chalk7 from "chalk";
862
872
  async function listApps(options) {
863
873
  const { token, baseUrl } = resolveAuth(options);
864
874
  const client = new ApiClient(baseUrl, token);
865
- const apps = await client.listApps();
875
+ let apps;
876
+ try {
877
+ apps = await client.listApps();
878
+ } catch (err) {
879
+ reportCommandError(options.output === "json", err);
880
+ process.exit(EXIT_INFRA_ERROR);
881
+ }
866
882
  if (options.output === "json") {
867
883
  console.log(JSON.stringify(apps, null, 2));
868
884
  return;
@@ -896,7 +912,7 @@ async function stopCommand(sessionId, opts, deps) {
896
912
  const message = err instanceof Error ? err.message : String(err);
897
913
  if (opts.json) console.log(JSON.stringify({ stopped: false, session_id: id, message }));
898
914
  console.error(chalk8.red(`error: ${message}`));
899
- process.exitCode = 1;
915
+ process.exitCode = errorExitCode(err);
900
916
  return;
901
917
  }
902
918
  const state = readDevState(process.cwd());
@@ -1278,8 +1294,10 @@ import chalk13 from "chalk";
1278
1294
  // src/lib/formatters/builds.ts
1279
1295
  import chalk12 from "chalk";
1280
1296
  var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
1281
- function formatCostCell(costMicro) {
1282
- return costMicro > 0 ? `$${(costMicro / 1e6).toFixed(2)}` : "";
1297
+ var UNBILLED_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
1298
+ function formatCostCell(costMicro, status) {
1299
+ if (costMicro <= 0 && UNBILLED_STATUSES.has(status)) return "-";
1300
+ return `$${(Math.max(costMicro, 0) / 1e6).toFixed(2)}`;
1283
1301
  }
1284
1302
  var STATUS_COLOR = {
1285
1303
  succeeded: chalk12.green,
@@ -1298,8 +1316,8 @@ function formatBuildsHuman(builds2) {
1298
1316
  b.id,
1299
1317
  b.platform,
1300
1318
  b.status,
1301
- b.created_at ?? "",
1302
- formatCostCell(b.cost_micro),
1319
+ formatUtcTimestamp(b.created_at),
1320
+ formatCostCell(b.cost_micro, b.status),
1303
1321
  b.app_public_id ?? ""
1304
1322
  ]);
1305
1323
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
@@ -1350,7 +1368,7 @@ async function buildsList(opts, deps) {
1350
1368
  process.exitCode = 0;
1351
1369
  } catch (err) {
1352
1370
  printError(opts, err);
1353
- process.exitCode = 1;
1371
+ process.exitCode = errorExitCode(err);
1354
1372
  }
1355
1373
  }
1356
1374
  async function buildsLogs(id, opts, deps) {
@@ -1373,7 +1391,7 @@ async function buildsLogs(id, opts, deps) {
1373
1391
  process.exitCode = final.status === "succeeded" ? 0 : 1;
1374
1392
  } catch (err) {
1375
1393
  printError(opts, err);
1376
- process.exitCode = 1;
1394
+ process.exitCode = errorExitCode(err);
1377
1395
  }
1378
1396
  }
1379
1397
  var MAX_CONSECUTIVE_POLL_FAILURES = 5;
@@ -1466,7 +1484,7 @@ async function buildsDownload(id, opts, deps) {
1466
1484
  process.exitCode = 0;
1467
1485
  } catch (err) {
1468
1486
  printError(opts, err);
1469
- process.exitCode = 1;
1487
+ process.exitCode = errorExitCode(err);
1470
1488
  }
1471
1489
  }
1472
1490
  async function buildsCancel(id, opts, deps) {
@@ -1483,7 +1501,7 @@ async function buildsCancel(id, opts, deps) {
1483
1501
  process.exitCode = 0;
1484
1502
  } catch (err) {
1485
1503
  printError(opts, err);
1486
- process.exitCode = 1;
1504
+ process.exitCode = errorExitCode(err);
1487
1505
  }
1488
1506
  }
1489
1507
  async function buildsDeleteArtifact(id, opts, deps) {
@@ -1500,7 +1518,7 @@ async function buildsDeleteArtifact(id, opts, deps) {
1500
1518
  process.exitCode = 0;
1501
1519
  } catch (err) {
1502
1520
  printError(opts, err);
1503
- process.exitCode = 1;
1521
+ process.exitCode = errorExitCode(err);
1504
1522
  }
1505
1523
  }
1506
1524
  function printError(opts, err) {
@@ -1692,7 +1710,7 @@ async function submitCommand(opts, deps) {
1692
1710
  process.exitCode = final.status === "succeeded" ? 0 : 1;
1693
1711
  } catch (err) {
1694
1712
  printError2(opts, err);
1695
- process.exitCode = 1;
1713
+ process.exitCode = errorExitCode(err);
1696
1714
  }
1697
1715
  }
1698
1716
  async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
@@ -1723,7 +1741,7 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
1723
1741
  process.exitCode = result.ready ? 0 : 1;
1724
1742
  } catch (err) {
1725
1743
  printError2(opts, err);
1726
- process.exitCode = 1;
1744
+ process.exitCode = errorExitCode(err);
1727
1745
  }
1728
1746
  }
1729
1747
  async function resolveLatestBuild(platform, deps) {
@@ -1853,7 +1871,7 @@ async function versionsList(opts, deps) {
1853
1871
  process.exitCode = 0;
1854
1872
  } catch (err) {
1855
1873
  printError3(opts, err);
1856
- process.exitCode = 1;
1874
+ process.exitCode = errorExitCode(err);
1857
1875
  }
1858
1876
  }
1859
1877
  async function versionsSet(buildNumber, opts, deps) {
@@ -1886,7 +1904,7 @@ async function versionsSet(buildNumber, opts, deps) {
1886
1904
  process.exitCode = 0;
1887
1905
  } catch (err) {
1888
1906
  printError3(opts, err);
1889
- process.exitCode = 1;
1907
+ process.exitCode = errorExitCode(err);
1890
1908
  }
1891
1909
  }
1892
1910
  var HEADERS2 = ["PLATFORM", "BUILD NUMBER", "UPDATED"];
@@ -1962,7 +1980,7 @@ Create an admin API token in the dashboard and use ${chalk16.bold("--token")} or
1962
1980
  console.log(JSON.stringify({ event: "error", message }));
1963
1981
  }
1964
1982
  console.error(chalk16.red(`error: ${message}`));
1965
- process.exitCode = 1;
1983
+ process.exitCode = errorExitCode(err);
1966
1984
  }
1967
1985
  }
1968
1986
 
@@ -2040,7 +2058,7 @@ async function envList(opts, deps) {
2040
2058
  process.exitCode = 0;
2041
2059
  } catch (err) {
2042
2060
  printError4(opts, err);
2043
- process.exitCode = 1;
2061
+ process.exitCode = errorExitCode(err);
2044
2062
  }
2045
2063
  }
2046
2064
  function formatEnvVarsHuman(variables) {
@@ -2089,7 +2107,7 @@ async function envSet(name, value, opts, deps) {
2089
2107
  process.exitCode = 0;
2090
2108
  } catch (err) {
2091
2109
  printError4(opts, err);
2092
- process.exitCode = 1;
2110
+ process.exitCode = errorExitCode(err);
2093
2111
  }
2094
2112
  }
2095
2113
  function resolveVisibility(opts) {
@@ -2122,7 +2140,7 @@ async function envRm(name, opts, deps) {
2122
2140
  process.exitCode = 0;
2123
2141
  } catch (err) {
2124
2142
  printError4(opts, err);
2125
- process.exitCode = 1;
2143
+ process.exitCode = errorExitCode(err);
2126
2144
  }
2127
2145
  }
2128
2146
  async function envPull(opts, deps) {
@@ -2154,7 +2172,7 @@ async function envPull(opts, deps) {
2154
2172
  process.exitCode = 0;
2155
2173
  } catch (err) {
2156
2174
  printError4(opts, err);
2157
- process.exitCode = 1;
2175
+ process.exitCode = errorExitCode(err);
2158
2176
  }
2159
2177
  }
2160
2178
  function isLossyEnvValue(value) {
@@ -2347,7 +2365,7 @@ async function credentialsAdd(opts, deps) {
2347
2365
  process.exitCode = 0;
2348
2366
  } catch (err) {
2349
2367
  printError5(opts, err);
2350
- process.exitCode = 1;
2368
+ process.exitCode = errorExitCode(err);
2351
2369
  }
2352
2370
  }
2353
2371
  async function credentialsGenerate(opts, deps) {
@@ -2421,7 +2439,7 @@ async function credentialsGenerate(opts, deps) {
2421
2439
  process.exitCode = 0;
2422
2440
  } catch (err) {
2423
2441
  printError5(opts, err);
2424
- process.exitCode = 1;
2442
+ process.exitCode = errorExitCode(err);
2425
2443
  }
2426
2444
  }
2427
2445
  async function generateAppleCredential(platform, opts, d) {
@@ -2457,7 +2475,7 @@ async function generateAppleCredential(platform, opts, d) {
2457
2475
  process.exitCode = 0;
2458
2476
  } catch (err) {
2459
2477
  printError5(opts, err);
2460
- process.exitCode = 1;
2478
+ process.exitCode = errorExitCode(err);
2461
2479
  }
2462
2480
  }
2463
2481
  async function credentialsDownload(name, opts, deps) {
@@ -2532,7 +2550,7 @@ async function credentialsDownload(name, opts, deps) {
2532
2550
  process.exitCode = 0;
2533
2551
  } catch (err) {
2534
2552
  printError5(opts, err);
2535
- process.exitCode = 1;
2553
+ process.exitCode = errorExitCode(err);
2536
2554
  }
2537
2555
  }
2538
2556
  async function ascKeyAdd(opts, deps) {
@@ -2572,7 +2590,7 @@ async function ascKeyAdd(opts, deps) {
2572
2590
  process.exitCode = 0;
2573
2591
  } catch (err) {
2574
2592
  printError5(opts, err);
2575
- process.exitCode = 1;
2593
+ process.exitCode = errorExitCode(err);
2576
2594
  }
2577
2595
  }
2578
2596
  async function ascKeyList(opts, deps) {
@@ -2587,7 +2605,7 @@ async function ascKeyList(opts, deps) {
2587
2605
  process.exitCode = 0;
2588
2606
  } catch (err) {
2589
2607
  printError5(opts, err);
2590
- process.exitCode = 1;
2608
+ process.exitCode = errorExitCode(err);
2591
2609
  }
2592
2610
  }
2593
2611
  function formatAscKeysHuman(keys) {
@@ -2595,7 +2613,7 @@ function formatAscKeysHuman(keys) {
2595
2613
  return "No App Store Connect keys found.\n";
2596
2614
  }
2597
2615
  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 ?? ""]);
2616
+ const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, formatUtcTimestamp(k.created_at)]);
2599
2617
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2600
2618
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2601
2619
  const join3 = (cells) => cells.join(" ").trimEnd();
@@ -2615,7 +2633,7 @@ async function ascKeyRm(name, opts, deps) {
2615
2633
  process.exitCode = 0;
2616
2634
  } catch (err) {
2617
2635
  printError5(opts, err);
2618
- process.exitCode = 1;
2636
+ process.exitCode = errorExitCode(err);
2619
2637
  }
2620
2638
  }
2621
2639
  async function playKeyAdd(opts, deps) {
@@ -2648,7 +2666,7 @@ async function playKeyAdd(opts, deps) {
2648
2666
  process.exitCode = 0;
2649
2667
  } catch (err) {
2650
2668
  printError5(opts, err);
2651
- process.exitCode = 1;
2669
+ process.exitCode = errorExitCode(err);
2652
2670
  }
2653
2671
  }
2654
2672
  async function playKeyList(opts, deps) {
@@ -2663,7 +2681,7 @@ async function playKeyList(opts, deps) {
2663
2681
  process.exitCode = 0;
2664
2682
  } catch (err) {
2665
2683
  printError5(opts, err);
2666
- process.exitCode = 1;
2684
+ process.exitCode = errorExitCode(err);
2667
2685
  }
2668
2686
  }
2669
2687
  function formatPlayKeysHuman(keys) {
@@ -2671,7 +2689,7 @@ function formatPlayKeysHuman(keys) {
2671
2689
  return "No Google Play keys found.\n";
2672
2690
  }
2673
2691
  const HEADERS3 = ["NAME", "CLIENT EMAIL", "CREATED"];
2674
- const rows = keys.map((k) => [k.name, k.client_email, k.created_at ?? ""]);
2692
+ const rows = keys.map((k) => [k.name, k.client_email, formatUtcTimestamp(k.created_at)]);
2675
2693
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2676
2694
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2677
2695
  const join3 = (cells) => cells.join(" ").trimEnd();
@@ -2691,7 +2709,7 @@ async function playKeyRm(name, opts, deps) {
2691
2709
  process.exitCode = 0;
2692
2710
  } catch (err) {
2693
2711
  printError5(opts, err);
2694
- process.exitCode = 1;
2712
+ process.exitCode = errorExitCode(err);
2695
2713
  }
2696
2714
  }
2697
2715
  async function credentialsList(opts, deps) {
@@ -2712,7 +2730,7 @@ async function credentialsList(opts, deps) {
2712
2730
  process.exitCode = 0;
2713
2731
  } catch (err) {
2714
2732
  printError5(opts, err);
2715
- process.exitCode = 1;
2733
+ process.exitCode = errorExitCode(err);
2716
2734
  }
2717
2735
  }
2718
2736
  function formatIdentity(c) {
@@ -2731,10 +2749,10 @@ function earliestExpiry(c) {
2731
2749
  function formatExpires(c) {
2732
2750
  const parts = [];
2733
2751
  if (c.profile_expires_at) {
2734
- if (c.expires_at) parts.push(`cert ${c.expires_at}`);
2735
- parts.push(`profile ${c.profile_expires_at}`);
2752
+ if (c.expires_at) parts.push(`cert ${formatUtcTimestamp(c.expires_at)}`);
2753
+ parts.push(`profile ${formatUtcTimestamp(c.profile_expires_at)}`);
2736
2754
  } else if (c.expires_at) {
2737
- parts.push(c.expires_at);
2755
+ parts.push(formatUtcTimestamp(c.expires_at));
2738
2756
  }
2739
2757
  if (parts.length === 0) return "";
2740
2758
  const cell = parts.join(" \xB7 ");
@@ -2771,7 +2789,7 @@ async function credentialsRm(name, opts, deps) {
2771
2789
  process.exitCode = 0;
2772
2790
  } catch (err) {
2773
2791
  printError5(opts, err);
2774
- process.exitCode = 1;
2792
+ process.exitCode = errorExitCode(err);
2775
2793
  }
2776
2794
  }
2777
2795
  function printKeystoreWriteFailure(opts, err, result) {
@@ -2849,14 +2867,14 @@ async function webhooksAdd(opts, deps) {
2849
2867
  process.exitCode = 0;
2850
2868
  } catch (err) {
2851
2869
  printError6(opts, err);
2852
- process.exitCode = 1;
2870
+ process.exitCode = errorExitCode(err);
2853
2871
  }
2854
2872
  }
2855
2873
  function formatLastDelivery(ld) {
2856
2874
  if (!ld) return "\u2014";
2857
2875
  if (ld.status === "pending") return "pending";
2858
2876
  const code = ld.response_code != null ? ` (${ld.response_code})` : "";
2859
- const at = ld.at != null ? ` ${ld.at}` : "";
2877
+ const at = ld.at != null ? ` ${formatUtcTimestamp(ld.at)}` : "";
2860
2878
  return `${ld.status}${code}${at}`;
2861
2879
  }
2862
2880
  function formatWebhooksHuman(webhooks2) {
@@ -2869,7 +2887,7 @@ function formatWebhooksHuman(webhooks2) {
2869
2887
  w.url,
2870
2888
  w.active ? "yes" : "no",
2871
2889
  formatLastDelivery(w.last_delivery),
2872
- w.created_at
2890
+ formatUtcTimestamp(w.created_at)
2873
2891
  ]);
2874
2892
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2875
2893
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
@@ -2890,7 +2908,7 @@ async function webhooksList(opts, deps) {
2890
2908
  process.exitCode = 0;
2891
2909
  } catch (err) {
2892
2910
  printError6(opts, err);
2893
- process.exitCode = 1;
2911
+ process.exitCode = errorExitCode(err);
2894
2912
  }
2895
2913
  }
2896
2914
  async function webhooksRm(id, opts, deps) {
@@ -2905,7 +2923,7 @@ async function webhooksRm(id, opts, deps) {
2905
2923
  process.exitCode = 0;
2906
2924
  } catch (err) {
2907
2925
  printError6(opts, err);
2908
- process.exitCode = 1;
2926
+ process.exitCode = errorExitCode(err);
2909
2927
  }
2910
2928
  }
2911
2929
  function parseUtcTimestamp(iso) {
@@ -2939,7 +2957,7 @@ async function webhooksTest(id, opts, deps) {
2939
2957
  } else {
2940
2958
  if (!opts.json) {
2941
2959
  const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
2942
- console.error(chalk19.red(`\u2717 Failed (${code}) at ${ld.at}`));
2960
+ console.error(chalk19.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
2943
2961
  }
2944
2962
  process.exitCode = 1;
2945
2963
  }
@@ -2955,7 +2973,7 @@ async function webhooksTest(id, opts, deps) {
2955
2973
  process.exitCode = 1;
2956
2974
  } catch (err) {
2957
2975
  printError6(opts, err);
2958
- process.exitCode = 1;
2976
+ process.exitCode = errorExitCode(err);
2959
2977
  }
2960
2978
  }
2961
2979
  function printError6(opts, err) {
@@ -2978,8 +2996,7 @@ async function listDevicesCommand(options) {
2978
2996
  }
2979
2997
  process.exit(EXIT_PASS);
2980
2998
  } catch (err) {
2981
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
2982
- `);
2999
+ reportCommandError(options.output === "json", err);
2983
3000
  process.exit(EXIT_INFRA_ERROR);
2984
3001
  }
2985
3002
  return;
@@ -2997,8 +3014,7 @@ async function listDevicesCommand(options) {
2997
3014
  }
2998
3015
  process.exit(EXIT_PASS);
2999
3016
  } catch (err) {
3000
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
3001
- `);
3017
+ reportCommandError(options.output === "json", err);
3002
3018
  process.exit(EXIT_INFRA_ERROR);
3003
3019
  }
3004
3020
  }
@@ -3271,7 +3287,11 @@ program.command("list-apps").description("List apps in your organization").optio
3271
3287
  program.command("list-builds <app>").description(
3272
3288
  "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
3289
  ).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(
3290
+ 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(
3291
+ new Option("--type <type>", "Filter by device type").choices(["ios", "android", "roku"])
3292
+ ).addOption(
3293
+ new Option("--category <category>", "Filter by category").choices(["phone", "tablet", "tv"])
3294
+ ).addOption(new Option("--status <status>", "Filter by status").choices(["available", "busy"])).option(
3275
3295
  "--models",
3276
3296
  "List the device models you can start instead (the names `dev --model` takes)",
3277
3297
  false
@@ -3445,8 +3465,8 @@ program.command("dev-reload").description(
3445
3465
  program.command("mcp").description(
3446
3466
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
3447
3467
  ).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);
3468
+ const { runMcpServer } = await import("./server-XJREB7KU.js");
3469
+ await runMcpServer({ ...opts, version: pkg.version });
3450
3470
  });
3451
3471
  program.command("agent-setup").description(
3452
3472
  "Make VibeView discoverable to your coding agent: install the vibeview-agent skill (and optionally register the MCP server with Claude Code)"
@@ -3456,6 +3476,8 @@ for (const cmd of AGENT_COMMANDS) {
3456
3476
  const flagParams = cmd.params.filter((p) => p.cli !== "positional");
3457
3477
  const argSignature = positionalParams.map((p) => p.required ? `<${p.name}>` : `[${p.name}]`).join(" ");
3458
3478
  let command = program.command(argSignature ? `${cmd.verb} ${argSignature}` : cmd.verb).description(cmd.summary);
3479
+ if (cmd.verb === "press") command.addHelpText("after", `
3480
+ ${pressButtonsHelp()}`);
3459
3481
  for (const param of flagParams) {
3460
3482
  const flagArg = param.type === "number" ? `${param.cli} <n>` : `${param.cli} <value>`;
3461
3483
  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-46X7UN6Y.js";
25
25
  import {
26
26
  ApiClient,
27
27
  resolveAuth
28
- } from "./chunk-RFFCCPGZ.js";
28
+ } from "./chunk-U7FCGMWG.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-QUUXKNR2.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-U7FCGMWG.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.0",
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). |