vibeview 0.38.0 → 0.40.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.
@@ -274,7 +274,7 @@ var ApiClient = class {
274
274
  }
275
275
  async getRunStatus(runId) {
276
276
  const data = await this.request("GET", `/runs/${runId}`);
277
- return { ...data, url: data.run_url ?? `${this.baseUrl}/tests/runs/${runId}` };
277
+ return { ...data, url: runResultUrl(data, `${this.baseUrl}/tests/runs/${runId}`) };
278
278
  }
279
279
  /** Request without /ci prefix — for non-CI endpoints like /apps, /tests */
280
280
  async requestRaw(method, path, body) {
@@ -772,6 +772,13 @@ var ApiClient = class {
772
772
  return this.requestRaw("POST", `/webhooks/${id}/test`);
773
773
  }
774
774
  };
775
+ function runResultUrl(data, fallback) {
776
+ const url = data.run_url ?? fallback;
777
+ if (!data.retry_run_id) return url;
778
+ const firstId = url.slice(url.lastIndexOf("/") + 1);
779
+ if (!firstId) return url;
780
+ return `${url.slice(0, url.length - firstId.length)}${encodeURIComponent(data.retry_run_id)}`;
781
+ }
775
782
  function parseContentDispositionFilename(header) {
776
783
  if (!header) return void 0;
777
784
  const extMatch = header.match(/filename\*\s*=\s*[^']*''([^;]+)/i);
@@ -870,6 +877,10 @@ function createSpinner(text, opts = {}) {
870
877
  var EXIT_PASS = 0;
871
878
  var EXIT_TEST_FAIL = 1;
872
879
  var EXIT_INFRA_ERROR = 2;
880
+ var RUN_COMMANDS = ["run-test", "run-suite", "run-suites"];
881
+ function runCommandParseExitCode(err) {
882
+ return err.exitCode === 0 ? EXIT_PASS : EXIT_INFRA_ERROR;
883
+ }
873
884
  var UsageError = class extends Error {
874
885
  constructor(message) {
875
886
  super(message);
@@ -1015,6 +1026,8 @@ export {
1015
1026
  EXIT_PASS,
1016
1027
  EXIT_TEST_FAIL,
1017
1028
  EXIT_INFRA_ERROR,
1029
+ RUN_COMMANDS,
1030
+ runCommandParseExitCode,
1018
1031
  UsageError,
1019
1032
  DEVICE_STATUS_FILTERS,
1020
1033
  queueReasonCopy,
@@ -13,7 +13,32 @@ import {
13
13
  resolveArtifact,
14
14
  resolveAuth,
15
15
  uploadAppBuild
16
- } from "./chunk-MYXUGCR5.js";
16
+ } from "./chunk-GDTLLJFL.js";
17
+
18
+ // src/lib/formatters/not-checked.ts
19
+ function notCheckedSteps(steps) {
20
+ return (steps ?? []).filter((s) => s.status === "not_checked");
21
+ }
22
+ function runStepsNotChecked(result) {
23
+ if (typeof result.steps_not_checked === "number") return result.steps_not_checked;
24
+ return notCheckedSteps(result.steps).length;
25
+ }
26
+ function stepsNotCheckedLabel(n) {
27
+ return `${n} step${n === 1 ? "" : "s"} not checked`;
28
+ }
29
+ function notCheckedSuffix(status, n) {
30
+ return status === "passed" && n && n > 0 ? ` (${stepsNotCheckedLabel(n)})` : "";
31
+ }
32
+ function outcomeOf(r) {
33
+ if (r.not_checked === true || r.status === "not_checked") return "not_checked";
34
+ if (r.not_run === true || r.status === "cancelled" && isNotRunMessage(r.error_message)) {
35
+ return "not_run";
36
+ }
37
+ return r.status ?? "";
38
+ }
39
+ function isNotRunMessage(message) {
40
+ return (message ?? "").startsWith("Not run:");
41
+ }
17
42
 
18
43
  // src/lib/dev-config.ts
19
44
  import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
@@ -5168,6 +5193,11 @@ async function runAgentVerb(cmd, positionals2, opts) {
5168
5193
  }
5169
5194
 
5170
5195
  export {
5196
+ notCheckedSteps,
5197
+ runStepsNotChecked,
5198
+ stepsNotCheckedLabel,
5199
+ notCheckedSuffix,
5200
+ outcomeOf,
5171
5201
  DEV_PLATFORMS,
5172
5202
  PLATFORM_NEEDS_METRO,
5173
5203
  isDevPlatform,
package/dist/cli.js CHANGED
@@ -15,6 +15,9 @@ import {
15
15
  listBuilds,
16
16
  loadDevConfig,
17
17
  newTransitionState,
18
+ notCheckedSteps,
19
+ notCheckedSuffix,
20
+ outcomeOf,
18
21
  platformLabel,
19
22
  platformWithArticle,
20
23
  pressButtonsHelp,
@@ -24,8 +27,10 @@ import {
24
27
  readDevStateFile,
25
28
  readMetroState,
26
29
  recordDevUse,
27
- runAgentVerb
28
- } from "./chunk-AV674D7T.js";
30
+ runAgentVerb,
31
+ runStepsNotChecked,
32
+ stepsNotCheckedLabel
33
+ } from "./chunk-MK3WDPVZ.js";
29
34
  import {
30
35
  ApiClient,
31
36
  ApiStatusError,
@@ -35,6 +40,7 @@ import {
35
40
  EXIT_TEST_FAIL,
36
41
  NetworkError,
37
42
  NotFoundError,
43
+ RUN_COMMANDS,
38
44
  UsageError,
39
45
  createSpinner,
40
46
  errorExitCode,
@@ -44,9 +50,10 @@ import {
44
50
  queueReasonCopy,
45
51
  reportCommandError,
46
52
  resolveAuth,
53
+ runCommandParseExitCode,
47
54
  saveConfig,
48
55
  uploadApp
49
- } from "./chunk-MYXUGCR5.js";
56
+ } from "./chunk-GDTLLJFL.js";
50
57
 
51
58
  // src/cli.ts
52
59
  import { createRequire } from "module";
@@ -61,21 +68,6 @@ import chalk2 from "chalk";
61
68
  // src/lib/formatters/human.ts
62
69
  import chalk from "chalk";
63
70
 
64
- // src/lib/formatters/not-checked.ts
65
- function notCheckedSteps(steps) {
66
- return (steps ?? []).filter((s) => s.status === "not_checked");
67
- }
68
- function runStepsNotChecked(result) {
69
- if (typeof result.steps_not_checked === "number") return result.steps_not_checked;
70
- return notCheckedSteps(result.steps).length;
71
- }
72
- function stepsNotCheckedLabel(n) {
73
- return `${n} step${n === 1 ? "" : "s"} not checked`;
74
- }
75
- function notCheckedSuffix(status, n) {
76
- return status === "passed" && n && n > 0 ? ` (${stepsNotCheckedLabel(n)})` : "";
77
- }
78
-
79
71
  // src/lib/formatters/cases.ts
80
72
  function resultCases(result) {
81
73
  if (Array.isArray(result.test_runs)) return result.test_runs;
@@ -91,6 +83,8 @@ function resultCases(result) {
91
83
  screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
92
84
  url: result.url,
93
85
  quarantined: result.quarantined,
86
+ not_checked: result.not_checked,
87
+ not_run: result.not_run,
94
88
  steps_not_checked: runStepsNotChecked(result),
95
89
  not_checked_steps: notCheckedSteps(result.steps).map((s) => ({
96
90
  step: s.step,
@@ -118,37 +112,50 @@ function failureLines(failure) {
118
112
  }
119
113
  function formatHuman(result, verbose) {
120
114
  const lines = [];
121
- const icon = result.status === "passed" ? chalk.green("PASS") : result.status === "failed" && result.quarantined === true ? chalk.yellow("FAIL (quarantined)") : chalk.red("FAIL");
115
+ const outcome = outcomeOf(result);
116
+ const icon = outcome === "not_checked" ? chalk.gray("NOT CHECKED") : result.status === "passed" ? chalk.green("PASS") : result.status === "failed" && result.quarantined === true ? chalk.yellow("FAIL (quarantined)") : chalk.red("FAIL");
122
117
  const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
123
118
  lines.push("");
124
119
  const title = result.suite_name || result.case_name || result.run_id;
125
120
  const cases = resultCases(result);
126
121
  const notChecked = cases.reduce((n, t) => n + (t.steps_not_checked ?? 0), 0);
127
- const runSuffix = notCheckedSuffix(result.status, notChecked);
122
+ const runSuffix = notCheckedSuffix(outcome, notChecked);
128
123
  lines.push(
129
124
  ` ${icon}${runSuffix ? chalk.yellow(runSuffix) : ""} ${chalk.bold(title)} ${chalk.dim(duration)}`
130
125
  );
131
- const passed = cases.filter((t) => t.status === "passed").length;
126
+ const passed = cases.filter((t) => outcomeOf(t) === "passed").length;
127
+ const notCheckedCases = cases.filter((t) => outcomeOf(t) === "not_checked").length;
132
128
  const total = cases.length;
133
- const notCheckedNote = notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
134
- lines.push(` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}`)}`);
129
+ const notCheckedNote = notCheckedCases > 0 ? `, ${notCheckedCases} not checked` : notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
130
+ const quarantined = cases.filter((t) => t.status === "failed" && t.quarantined === true).length;
131
+ const quarantinedNote = quarantined > 0 && (result.test_runs ?? []).length > 0 ? `, ${quarantined} failed (quarantined)` : "";
132
+ lines.push(
133
+ ` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}${quarantinedNote}`)}`
134
+ );
135
135
  if (result.url) {
136
- lines.push(` ${chalk.dim(result.url)}`);
136
+ lines.push(` ${chalk.dim(result.first_attempt ? `Retry: ${result.url}` : result.url)}`);
137
+ }
138
+ if (result.first_attempt) {
139
+ const first = result.first_attempt;
140
+ lines.push(` ${chalk.dim(`First attempt (${first.status ?? "unknown"}): ${first.url}`)}`);
137
141
  }
138
142
  const suiteCases = result.test_runs ?? [];
139
143
  if (suiteCases.length > 0) {
140
144
  lines.push("");
141
145
  for (const tr of suiteCases) {
142
- const caseIcon = tr.status === "passed" ? chalk.green(" PASS") : tr.status === "failed" && tr.quarantined === true ? chalk.yellow(" FAIL (quarantined)") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
146
+ const caseOutcome = outcomeOf(tr);
147
+ const caseIcon = caseOutcome === "not_checked" ? chalk.gray(" NOT CHECKED") : caseOutcome === "not_run" ? chalk.gray(" NOT RUN") : tr.status === "passed" ? chalk.green(" PASS") : tr.status === "failed" && tr.quarantined === true ? chalk.yellow(" FAIL (quarantined)") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
143
148
  const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
144
- const caseSuffix = notCheckedSuffix(tr.status, tr.steps_not_checked);
149
+ const caseSuffix = notCheckedSuffix(caseOutcome, tr.steps_not_checked);
145
150
  lines.push(
146
151
  ` ${caseIcon}${caseSuffix ? chalk.yellow(caseSuffix) : ""} ${tr.case_name} ${chalk.dim(caseDuration)}`
147
152
  );
148
153
  if (tr.url) {
149
154
  lines.push(` ${chalk.dim(tr.url)}`);
150
155
  }
151
- if (tr.status !== "passed") {
156
+ if (caseOutcome === "not_checked" || caseOutcome === "not_run") {
157
+ if (tr.error_message) lines.push(` ${chalk.dim(tr.error_message)}`);
158
+ } else if (tr.status !== "passed") {
152
159
  lines.push(...failureLines(tr));
153
160
  }
154
161
  }
@@ -174,7 +181,10 @@ function formatHuman(result, verbose) {
174
181
  }
175
182
  }
176
183
  }
177
- if (result.error_message) {
184
+ if (result.error_message && outcome === "not_checked") {
185
+ lines.push(`
186
+ ${chalk.dim(result.error_message)}`);
187
+ } else if (result.error_message) {
178
188
  lines.push(`
179
189
  ${chalk.red("Error:")} ${result.error_message}`);
180
190
  }
@@ -216,13 +226,18 @@ function caseDetailLines(c) {
216
226
  return lines;
217
227
  }
218
228
  var isQuarantinedFailure = (t) => t.status === "failed" && t.quarantined === true;
229
+ var NEUTRAL = /* @__PURE__ */ new Set(["not_checked", "not_run"]);
230
+ function neutralMessage(outcome, message) {
231
+ if (outcome === "not_checked") return `not checked: ${message || "no step was checked"}`;
232
+ return message || "Not run";
233
+ }
219
234
  function formatJunit(result) {
220
235
  const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
221
236
  const cases = resultCases(result);
222
237
  const tests = cases.length;
223
238
  const failures = cases.filter((t) => t.status === "failed" && !isQuarantinedFailure(t)).length;
224
239
  const errors = cases.filter((t) => t.status === "error").length;
225
- const skipped = cases.filter(isQuarantinedFailure).length;
240
+ const skipped = cases.filter((t) => isQuarantinedFailure(t) || NEUTRAL.has(outcomeOf(t))).length;
226
241
  const skippedAttr = skipped > 0 ? ` skipped="${skipped}"` : "";
227
242
  const totalTime = result.duration_ms ? (result.duration_ms / 1e3).toFixed(1) : "0";
228
243
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -249,8 +264,9 @@ function formatJunit(result) {
249
264
  for (const tr of cases) {
250
265
  const caseName = escapeXml(tr.case_name);
251
266
  const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
252
- const detail = caseDetailLines(tr);
253
- if (tr.status === "passed" && detail.length === 0) {
267
+ const outcome = outcomeOf(tr);
268
+ const detail = caseDetailLines({ ...tr, status: outcome });
269
+ if (outcome === "passed" && detail.length === 0) {
254
270
  lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
255
271
  continue;
256
272
  }
@@ -258,6 +274,10 @@ function formatJunit(result) {
258
274
  if (isQuarantinedFailure(tr)) {
259
275
  const msg = escapeXml(tr.error_message || "Test failed");
260
276
  lines.push(` <skipped message="quarantined: ${msg}" />`);
277
+ } else if (NEUTRAL.has(outcome)) {
278
+ lines.push(
279
+ ` <skipped message="${escapeXml(neutralMessage(outcome, tr.error_message))}" />`
280
+ );
261
281
  } else if (tr.status === "failed") {
262
282
  const msg = escapeXml(tr.error_message || "Test failed");
263
283
  lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
@@ -294,7 +314,7 @@ function formatJunitError(name, message, url) {
294
314
  lines.push("</testsuites>");
295
315
  return lines.join("\n");
296
316
  }
297
- var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
317
+ var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
298
318
  function caseLines(c, classname) {
299
319
  const name = escapeXml(c.name);
300
320
  const head = ` <testcase name="${name}" classname="${classname}" time="${c.time}"`;
@@ -331,6 +351,14 @@ function formatMultiSuiteJunit(result, opts = {}) {
331
351
  url: c.url || void 0,
332
352
  steps_not_checked: c.steps_not_checked
333
353
  };
354
+ const outcome = outcomeOf(c);
355
+ if (NEUTRAL.has(outcome)) {
356
+ return {
357
+ ...base,
358
+ verdict: "skipped",
359
+ message: neutralMessage(outcome, c.error_message)
360
+ };
361
+ }
334
362
  if (c.status === "failed" && c.quarantined === true) {
335
363
  return {
336
364
  ...base,
@@ -409,6 +437,7 @@ function formatMultiSuiteJunit(result, opts = {}) {
409
437
  }
410
438
 
411
439
  // src/lib/poll.ts
440
+ var TERMINAL_STATUSES = ["passed", "failed", "error", "cancelled", "not_checked"];
412
441
  function resumeSpinner(spinner, text) {
413
442
  if (spinner.isEnabled === false) {
414
443
  if (text !== void 0) spinner.text = text;
@@ -506,20 +535,26 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
506
535
  `${chalk2.cyan("\u25B6")} ${chalk2.bold(tr.case_name || "Case")} started`
507
536
  );
508
537
  }
509
- const isTerminal = ["passed", "failed", "error", "cancelled"].includes(cur);
510
- const wasTerminal = prev && ["passed", "failed", "error", "cancelled"].includes(prev);
538
+ const isTerminal = TERMINAL_STATUSES.includes(cur);
539
+ const wasTerminal = prev && TERMINAL_STATUSES.includes(prev);
511
540
  if (isTerminal && !wasTerminal) {
512
- const icon = cur === "passed" ? chalk2.green("\u2713") : cur === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
541
+ const outcome = outcomeOf(tr);
542
+ const neutral = outcome === "not_checked" || outcome === "not_run";
543
+ const icon = neutral ? chalk2.gray("\u25CB") : cur === "passed" ? chalk2.green("\u2713") : cur === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
544
+ const label = neutral ? outcome.replace("_", " ") : cur;
513
545
  const dur = tr.duration_ms ? ` (${(tr.duration_ms / 1e3).toFixed(1)}s)` : "";
514
- printBelowSpinner(spinner, `${icon} ${chalk2.bold(tr.case_name || "Case")} ${cur}${dur}`);
515
- if (tr.error_message && cur !== "passed") {
546
+ printBelowSpinner(
547
+ spinner,
548
+ `${icon} ${chalk2.bold(tr.case_name || "Case")} ${label}${dur}`
549
+ );
550
+ if (tr.error_message && cur !== "passed" && !neutral) {
516
551
  printBelowSpinner(spinner, ` ${chalk2.red(tr.error_message)}`);
517
552
  }
518
553
  }
519
554
  caseStateBefore.set(tr.public_id, cur);
520
555
  }
521
556
  }
522
- if (["passed", "failed", "error", "cancelled"].includes(result.status)) {
557
+ if (TERMINAL_STATUSES.includes(result.status)) {
523
558
  if (printedSteps.size > 0 && Array.isArray(result.steps)) {
524
559
  spinner.stop();
525
560
  result.steps.forEach((st, i) => {
@@ -569,7 +604,7 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
569
604
  if (result.type === "suite_run" && Array.isArray(result.test_runs)) {
570
605
  const total = result.total_cases ?? result.test_runs.length;
571
606
  const done = result.test_runs.filter(
572
- (tr) => ["passed", "failed", "error", "cancelled"].includes(tr.status ?? "")
607
+ (tr) => TERMINAL_STATUSES.includes(tr.status ?? "")
573
608
  ).length;
574
609
  spinner.text = `Running suite \u2014 ${done}/${total} cases complete...`;
575
610
  } else {
@@ -666,10 +701,10 @@ function displayProgressEvent(evt, ctx) {
666
701
  break;
667
702
  }
668
703
  case "run_completed": {
669
- const icon = evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
704
+ const icon = evt.status === "not_checked" ? chalk2.gray("\u25CB") : evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
670
705
  spinner.stop();
671
706
  process.stderr.write(
672
- `${icon} Run ${evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
707
+ `${icon} Run ${evt.status === "not_checked" ? "not checked" : evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
673
708
  `
674
709
  );
675
710
  resumeSpinner(spinner);
@@ -709,7 +744,7 @@ function outputResult(result, options) {
709
744
  }
710
745
  }
711
746
  function mapExitCode(status) {
712
- if (status === "passed") return EXIT_PASS;
747
+ if (status === "passed" || status === "not_checked") return EXIT_PASS;
713
748
  if (status === "failed") return EXIT_TEST_FAIL;
714
749
  return EXIT_INFRA_ERROR;
715
750
  }
@@ -832,7 +867,8 @@ ${signal} received.
832
867
  device: options.device,
833
868
  build: options.build,
834
869
  metadata: options.metadata,
835
- retryFailed: options.retry === false ? false : void 0,
870
+ // Opt-in: always sent, so an older server's CI default never retries.
871
+ retryFailed: options.retry === true,
836
872
  platform: options.platform,
837
873
  app: options.app
838
874
  });
@@ -960,7 +996,8 @@ ${signal} received.
960
996
  device_id: opts.device,
961
997
  build_id: opts.build,
962
998
  metadata: opts.metadata,
963
- retry_failed: opts.retry === false ? false : void 0
999
+ // Opt-in: always sent, so an older server's CI default never retries.
1000
+ retry_failed: opts.retry === true
964
1001
  });
965
1002
  } catch (err) {
966
1003
  spinner.stop();
@@ -1021,7 +1058,7 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
1021
1058
  pending = [];
1022
1059
  if (nextText !== null) spinner.start(nextText);
1023
1060
  };
1024
- const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
1061
+ const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
1025
1062
  let last = null;
1026
1063
  const abandon = async (reason) => {
1027
1064
  flush(null);
@@ -1080,10 +1117,11 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
1080
1117
  }
1081
1118
  const wasTerminal = prev !== void 0 && TERMINAL.has(prev);
1082
1119
  if (TERMINAL.has(cur) && !wasTerminal) {
1083
- const mark = cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
1120
+ const nothingChecked = outcomeOf(sr) === "not_checked";
1121
+ const mark = nothingChecked ? chalk5.gray("\u25CB") : cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
1084
1122
  const link = sr.url ? chalk5.dim(" " + sr.url) : "";
1085
1123
  print(
1086
- `${mark} ${chalk5.bold(sr.suite_name)} ${cur}: ${sr.passed} passed, ${sr.failed} failed${link}`
1124
+ `${mark} ${chalk5.bold(sr.suite_name)} ${nothingChecked ? "not checked" : cur}: ${suiteCounts(sr)}${link}`
1087
1125
  );
1088
1126
  if (sr.error_message) {
1089
1127
  print(` ${chalk5.red(sr.error_message)}`);
@@ -1137,39 +1175,57 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
1137
1175
  0
1138
1176
  );
1139
1177
  const aggregateNote = totalNotChecked > 0 ? chalk5.yellow(` (${stepsNotCheckedLabel(totalNotChecked)})`) : "";
1178
+ const aggregate = outcomeOf(last) === "not_checked" ? "not checked" : last.status;
1140
1179
  process.stdout.write(`
1141
- Aggregate: ${last.status}${aggregateNote}
1180
+ Aggregate: ${aggregate}${aggregateNote}
1142
1181
  `);
1143
1182
  for (const sr of last.suite_runs) {
1144
- const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
1183
+ const mark = outcomeOf(sr) === "not_checked" ? chalk5.gray("\u25CB") : sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
1145
1184
  const link = sr.url ? chalk5.dim(` ${sr.url}`) : "";
1146
- process.stdout.write(
1147
- ` ${mark} ${sr.suite_name}: ${sr.passed} passed, ${sr.failed} failed${link}
1148
- `
1149
- );
1185
+ process.stdout.write(` ${mark} ${sr.suite_name}: ${suiteCounts(sr)}${link}
1186
+ `);
1150
1187
  if (sr.error_message) {
1151
1188
  process.stdout.write(` ${chalk5.red(sr.error_message)}
1152
1189
  `);
1153
1190
  }
1154
1191
  for (const cr of sr.case_runs ?? []) {
1155
- const caseMark = cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
1192
+ const outcome = outcomeOf(cr);
1193
+ const neutral = outcome === "not_checked" || outcome === "not_run";
1194
+ const caseMark = neutral ? chalk5.gray("\u25CB") : cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : cr.status === "failed" && cr.quarantined === true ? chalk5.yellow("\u2717") : chalk5.red("\u2717");
1156
1195
  const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
1157
- const unchecked = notCheckedSuffix(cr.status, cr.steps_not_checked);
1196
+ const unchecked = notCheckedSuffix(outcome, cr.steps_not_checked);
1197
+ const quarantined = cr.status === "failed" && cr.quarantined === true;
1198
+ const label = neutral ? outcome.replace("_", " ") : quarantined ? "failed (quarantined)" : cr.status ?? "?";
1158
1199
  process.stdout.write(
1159
- ` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
1200
+ ` ${caseMark} ${cr.case_name ?? "Case"} ${label}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
1160
1201
  `
1161
1202
  );
1162
- if (cr.error_message && cr.status !== "passed") {
1203
+ if (cr.error_message && (neutral || cr.status === "failed" && cr.quarantined === true)) {
1204
+ process.stdout.write(` ${chalk5.dim(cr.error_message)}
1205
+ `);
1206
+ } else if (cr.error_message && cr.status !== "passed") {
1163
1207
  process.stdout.write(` ${chalk5.red(cr.error_message)}
1164
1208
  `);
1165
1209
  }
1166
1210
  }
1167
1211
  }
1168
1212
  }
1169
- if (last.status === "passed") process.exit(EXIT_PASS);
1213
+ if (last.status === "passed" || last.status === "not_checked") process.exit(EXIT_PASS);
1170
1214
  if (last.status === "failed") process.exit(EXIT_TEST_FAIL);
1171
1215
  process.exit(EXIT_INFRA_ERROR);
1172
1216
  }
1217
+ function suiteCounts(sr) {
1218
+ const nc = sr.not_checked_tests ?? 0;
1219
+ const q = Math.min(
1220
+ sr.failed,
1221
+ sr.quarantined_failures ?? (sr.case_runs ?? []).filter((c) => c.status === "failed" && c.quarantined === true).length
1222
+ );
1223
+ const parts = [`${sr.passed} passed`];
1224
+ if (q === 0 || sr.failed > q) parts.push(`${sr.failed - q} failed`);
1225
+ if (q > 0) parts.push(`${q} failed (quarantined)`);
1226
+ if (nc > 0) parts.push(`${nc} not checked`);
1227
+ return parts.join(", ");
1228
+ }
1173
1229
 
1174
1230
  // src/commands/run-test.ts
1175
1231
  import chalk6 from "chalk";
@@ -1238,7 +1294,8 @@ ${signal} received.
1238
1294
  device: options.device,
1239
1295
  build: options.build,
1240
1296
  metadata: options.metadata,
1241
- retryFailed: options.retry === false ? false : void 0,
1297
+ // Opt-in: always sent, so an older server's CI default never retries.
1298
+ retryFailed: options.retry === true,
1242
1299
  platform: options.platform,
1243
1300
  app: options.app
1244
1301
  });
@@ -1641,7 +1698,7 @@ function formatImport(result, baseUrl) {
1641
1698
  }
1642
1699
  for (const f of result.flags) lines.push(flagLine(f));
1643
1700
  for (const t of [...result.tests, ...result.setups]) {
1644
- const meta = [`${t.steps.length} steps`];
1701
+ const meta = [`${t.steps.length} ${t.steps.length === 1 ? "step" : "steps"}`];
1645
1702
  if (t.setup_keys.length) meta.push(`before this test: ${t.setup_keys.join(", ")}`);
1646
1703
  if (t.start_state) meta.push(`start: ${START_STATE_LABEL[t.start_state] ?? t.start_state}`);
1647
1704
  const head = t.importable ? t.name : chalk11.red(`${t.name} (not imported)`);
@@ -4040,9 +4097,9 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
4040
4097
  "--app <app-id>",
4041
4098
  "Run the suite against this app instead of its own (e.g. another platform build with a different bundle id)"
4042
4099
  ).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
4043
- "--no-retry",
4044
- "Do not retry a failed test once (CI retries a failed test once by default)"
4045
- ).action((suiteId, opts) => {
4100
+ "--retry",
4101
+ "Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
4102
+ ).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((suiteId, opts) => {
4046
4103
  if (opts.device && opts.deviceCategory) {
4047
4104
  process.stderr.write("error: --device cannot be combined with --device-category\n");
4048
4105
  process.exit(2);
@@ -4079,9 +4136,9 @@ program.command("run-suites").description("Run all suites matching a tag filter"
4079
4136
  "--device <device_id>",
4080
4137
  "Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
4081
4138
  ).option("--build <id>", "AppBuild id (validated against every matched suite)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
4082
- "--no-retry",
4083
- "Do not retry a failed test once (CI retries a failed test once by default)"
4084
- ).action((opts) => {
4139
+ "--retry",
4140
+ "Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
4141
+ ).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((opts) => {
4085
4142
  if (opts.device && opts.deviceCategory) {
4086
4143
  process.stderr.write("error: --device cannot be combined with --device-category\n");
4087
4144
  process.exit(2);
@@ -4115,9 +4172,9 @@ program.command("run-test <test-id>").description("Run a single test case").opti
4115
4172
  "--app <app-id>",
4116
4173
  "Run the test against this app instead of its own (e.g. another platform build with a different bundle id)"
4117
4174
  ).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
4118
- "--no-retry",
4119
- "Do not retry a failed test once (CI retries a failed test once by default)"
4120
- ).action((testId, opts) => {
4175
+ "--retry",
4176
+ "Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
4177
+ ).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((testId, opts) => {
4121
4178
  if (opts.device && opts.deviceCategory) {
4122
4179
  process.stderr.write("error: --device cannot be combined with --device-category\n");
4123
4180
  process.exit(2);
@@ -4349,7 +4406,7 @@ program.command("dev-reload").description(
4349
4406
  program.command("mcp").description(
4350
4407
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4351
4408
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4352
- const { runMcpServer } = await import("./server-FMU3HQWF.js");
4409
+ const { runMcpServer } = await import("./server-56MR4KJW.js");
4353
4410
  await runMcpServer({ ...opts, version: pkg.version });
4354
4411
  });
4355
4412
  program.command("agent-setup").description(
@@ -4373,6 +4430,9 @@ ${pressButtonsHelp()}`);
4373
4430
  return runAgentVerb(cmd, positionals, opts);
4374
4431
  });
4375
4432
  }
4433
+ for (const name of RUN_COMMANDS) {
4434
+ program.commands.find((c) => c.name() === name)?.exitOverride((err) => process.exit(runCommandParseExitCode(err)));
4435
+ }
4376
4436
  if (updateCheckAllowed(process.argv, process.env, process.stderr.isTTY)) {
4377
4437
  void checkForUpdate(pkg.version, { print: (line) => process.stderr.write(`${line}
4378
4438
  `) });
@@ -20,20 +20,22 @@ import {
20
20
  isDevPlatform,
21
21
  loadDevConfig,
22
22
  loadLocalDevConfig,
23
+ outcomeOf,
23
24
  parseBuildId,
24
25
  parseIdleWarningSeconds,
26
+ platformLabel,
25
27
  platformWithArticle,
26
28
  readDevState,
27
29
  recordDevUse,
28
30
  renderEnvelope,
29
31
  resolveMetro,
30
32
  startDevSession
31
- } from "./chunk-AV674D7T.js";
33
+ } from "./chunk-MK3WDPVZ.js";
32
34
  import {
33
35
  ApiClient,
34
36
  NotFoundError,
35
37
  resolveAuth
36
- } from "./chunk-MYXUGCR5.js";
38
+ } from "./chunk-GDTLLJFL.js";
37
39
 
38
40
  // src/mcp/server.ts
39
41
  import { writeFileSync } from "fs";
@@ -58,7 +60,12 @@ var CREATE_TEST_DEF = {
58
60
  app: { type: "string", description: "App id (from list_apps)." },
59
61
  name: { type: "string", description: "Test name." },
60
62
  steps: { type: "string", description: "The steps, one per line." },
61
- group: { type: "string", description: "Group id to add the test to (optional)." }
63
+ group: { type: "string", description: "Group id to add the test to (optional)." },
64
+ if_exists: {
65
+ type: "string",
66
+ description: "When a test with this name already exists for the app: update replaces it, new creates another one beside it, fail refuses. Omitted: an identical test is reused, a different one is refused.",
67
+ enum: ["fail", "update", "new"]
68
+ }
62
69
  },
63
70
  required: ["app", "name", "steps"]
64
71
  };
@@ -71,7 +78,7 @@ var RUN_TEST_DEF = {
71
78
  session_id: { type: "string", description: "Run on this session instead of a new device." },
72
79
  device_model: {
73
80
  type: "string",
74
- description: "Device model for a new device (from list_device_models); omitted: any free one."
81
+ description: "Device model for a new device (from list_device_models); omitted: any free one. A model of another platform (same form factor) runs the test's app for that platform: the app linked in its app family, else the app with the same bundle id / package. The reply names the platform and app the run went to."
75
82
  },
76
83
  ai_use: AI_USE_PROPERTY,
77
84
  start_from_current_screen: {
@@ -93,7 +100,7 @@ var RUN_GROUP_DEF = {
93
100
  session_id: { type: "string", description: "Run on this session instead of a new device." },
94
101
  device_model: {
95
102
  type: "string",
96
- description: "Device model for a new device; omitted: any free one."
103
+ description: "Device model for a new device; omitted: any free one. A model of another platform (same form factor) runs the group's app for that platform: the app linked in its app family, else the app with the same bundle id / package. The reply names the platform and app the run went to."
97
104
  },
98
105
  ai_use: AI_USE_PROPERTY,
99
106
  start_from_current_screen: {
@@ -346,6 +353,14 @@ async function explainInactive(sessionId, deps) {
346
353
  function findCommand(toolName) {
347
354
  return AGENT_COMMANDS.find((c) => toolNameFor(c.verb) === toolName);
348
355
  }
356
+ function testToolErrorMessage(err) {
357
+ const message = err instanceof Error ? err.message : String(err);
358
+ if (err instanceof NotFoundError && message.startsWith("Not found: ")) {
359
+ const detail = message.slice("Not found: ".length);
360
+ if (/not found/i.test(detail)) return detail;
361
+ }
362
+ return message;
363
+ }
349
364
  async function handleCreateTest(args, deps) {
350
365
  const steps = String(args.steps ?? "").split("\n").map((s) => s.trim()).filter(Boolean).map((text) => ({ text }));
351
366
  if (!steps.length) return errorResult("create_test: `steps` needs at least one line");
@@ -354,7 +369,8 @@ async function handleCreateTest(args, deps) {
354
369
  name: String(args.name),
355
370
  app_id: String(args.app),
356
371
  steps,
357
- group_ids: typeof args.group === "string" && args.group ? [args.group] : void 0
372
+ group_ids: typeof args.group === "string" && args.group ? [args.group] : void 0,
373
+ if_exists: args.if_exists === "fail" || args.if_exists === "update" || args.if_exists === "new" ? args.if_exists : void 0
358
374
  });
359
375
  const verb = created.outcome === "unchanged" ? "Test already exists, unchanged" : created.outcome === "updated" ? "Updated existing test" : "Created test";
360
376
  return textResult(
@@ -362,9 +378,17 @@ async function handleCreateTest(args, deps) {
362
378
  created
363
379
  );
364
380
  } catch (err) {
365
- return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
381
+ return errorResult(`error: ${testToolErrorMessage(err)}`);
366
382
  }
367
383
  }
384
+ function runDestination(lanes) {
385
+ return lanes.map((l) => {
386
+ const label = platformLabel(l.platform);
387
+ const device = l.model ? ` (${l.model})` : "";
388
+ const app = l.app_name ? `: ${l.app_name}, the ${label} app` : "";
389
+ return ` Running on ${label}${device}${app}.`;
390
+ }).join("");
391
+ }
368
392
  async function handleRun(kind, args, deps) {
369
393
  const id = String(kind === "test" ? args.test : args.group);
370
394
  const mode = typeof args.ai_use === "string" ? AI_USE_MODE[args.ai_use] : void 0;
@@ -386,12 +410,13 @@ async function handleRun(kind, args, deps) {
386
410
  kind === "test" ? { target: "test", test_ids: [id], devices, mode } : { target: "group", group_id: id, devices, mode }
387
411
  );
388
412
  const queued = r.lanes.some((l) => l.status === "queued") ? " It is waiting for a free device." : "";
413
+ const where = runDestination(r.lanes);
389
414
  return textResult(
390
- `Started run ${r.batch_id}.${queued} Check it with get_run run_id=${r.batch_id}.`,
415
+ `Started run ${r.batch_id}.${where}${queued} Check it with get_run run_id=${r.batch_id}.`,
391
416
  r
392
417
  );
393
418
  } catch (err) {
394
- return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
419
+ return errorResult(`error: ${testToolErrorMessage(err)}`);
395
420
  }
396
421
  }
397
422
  function notCheckedNote(status, n) {
@@ -399,33 +424,63 @@ function notCheckedNote(status, n) {
399
424
  return ` \u2014 ${n} step${n === 1 ? "" : "s"} not checked (no recorded actions; not verified)`;
400
425
  }
401
426
  var RUN_IN_PROGRESS = /* @__PURE__ */ new Set(["queued", "pending", "running"]);
427
+ function agentStatus(r) {
428
+ const outcome = outcomeOf(r);
429
+ return outcome === "not_checked" || outcome === "not_run" ? outcome.replace("_", " ") : outcome;
430
+ }
431
+ function testsNotChecked(n) {
432
+ return typeof n === "number" && n > 0 ? `, ${n} not checked` : "";
433
+ }
434
+ function quarantinedFailure(t) {
435
+ return t?.status === "failed" && t?.quarantined === true;
436
+ }
437
+ function quarantinedCount(n) {
438
+ return typeof n === "number" && n > 0 ? `, ${n} failed (quarantined)` : "";
439
+ }
402
440
  function formatRunForAgent(run) {
403
441
  const lines = [];
404
442
  if (run.lanes) {
443
+ const rowQuarantined = (r) => typeof r.tests_quarantined === "number" ? r.tests_quarantined : (r.tests ?? []).filter(quarantinedFailure).length;
444
+ let totalQuarantined = run.summary.tests_quarantined;
445
+ if (typeof totalQuarantined !== "number") {
446
+ totalQuarantined = 0;
447
+ for (const lane of run.lanes) {
448
+ for (const r of lane.runs ?? []) totalQuarantined += rowQuarantined(r);
449
+ }
450
+ }
405
451
  lines.push(
406
- `${run.name}: ${run.status} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed`
452
+ `${run.name}: ${agentStatus(run)} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed${testsNotChecked(run.summary.tests_not_checked)}${quarantinedCount(totalQuarantined)}`
407
453
  );
408
454
  for (const lane of run.lanes) {
409
455
  const detail = [lane.platform, lane.os_version].filter(Boolean).join(" ");
410
- let head = `${lane.model ? `${lane.model} (${detail})` : detail}: ${lane.status}`;
456
+ let head = `${lane.model ? `${lane.model} (${detail})` : detail}: ${agentStatus(lane)}`;
411
457
  if (lane.status === "queued" && typeof lane.queue?.position === "number") {
412
458
  head += ` (position ${lane.queue.position} in the queue)`;
413
459
  }
414
460
  if (lane.error_message) head += ` \u2014 ${lane.error_message}`;
415
461
  lines.push(head);
416
462
  for (const r of lane.runs ?? []) {
417
- const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed)` : "";
463
+ const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed${testsNotChecked(r.tests_not_checked)}${quarantinedCount(rowQuarantined(r))})` : "";
464
+ const q = r.kind === "test" && quarantinedFailure(r.tests?.[0] ?? {}) ? " (quarantined)" : "";
418
465
  lines.push(
419
- ` ${r.name}: ${r.status}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
466
+ ` ${r.name}: ${agentStatus(r)}${q}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
420
467
  );
468
+ if (r.kind !== "group") continue;
469
+ for (const t of r.tests ?? []) {
470
+ if (t.status !== "failed" && t.status !== "error") continue;
471
+ const tq = quarantinedFailure(t) ? " (quarantined)" : "";
472
+ lines.push(
473
+ ` ${t.name}: ${t.status}${tq}${t.error_message ? ` \u2014 ${t.error_message}` : ""}`
474
+ );
475
+ }
421
476
  }
422
477
  }
423
478
  } else if (Array.isArray(run.test_runs)) {
424
- lines.push(`${run.suite_name ?? "Group"}: ${run.status}`);
479
+ lines.push(`${run.suite_name ?? "Group"}: ${agentStatus(run)}`);
425
480
  for (const t of run.test_runs) {
426
481
  const q = t.quarantined && t.status === "failed" ? " (quarantined)" : "";
427
482
  lines.push(
428
- ` ${t.case_name}: ${t.status}${q}${t.error_message ? ` \u2014 ${t.error_message}` : ""}${notCheckedNote(t.status, t.steps_not_checked)}`
483
+ ` ${t.case_name}: ${agentStatus(t)}${q}${t.error_message ? ` \u2014 ${t.error_message}` : ""}${notCheckedNote(outcomeOf(t), t.steps_not_checked)}`
429
484
  );
430
485
  }
431
486
  } else {
@@ -434,7 +489,7 @@ function formatRunForAgent(run) {
434
489
  const notChecked = typeof run.steps_not_checked === "number" ? run.steps_not_checked : (run.steps ?? []).filter((s) => s.status === "not_checked").length;
435
490
  const q = run.quarantined && run.status === "failed" ? " (quarantined)" : "";
436
491
  lines.push(
437
- `${name}: ${run.status}${q}${run.error_message ? ` \u2014 ${run.error_message}` : ""}${notCheckedNote(run.status, notChecked)}`
492
+ `${name}: ${agentStatus(run)}${q}${run.error_message ? ` \u2014 ${run.error_message}` : ""}${notCheckedNote(outcomeOf(run), notChecked)}`
438
493
  );
439
494
  if (failed) {
440
495
  lines.push(
@@ -455,7 +510,7 @@ async function handleGetRun(args, deps) {
455
510
  const run = kind.kind === "batch" ? await client.getRunBatch(id) : await client.getRunStatus(id);
456
511
  return textResult(formatRunForAgent(run), run);
457
512
  } catch (err) {
458
- return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
513
+ return errorResult(`error: ${testToolErrorMessage(err)}`);
459
514
  }
460
515
  }
461
516
  async function dispatchToolCall(toolName, args, deps, extra = {}) {
@@ -874,7 +929,7 @@ async function handleUploadApp(args, deps) {
874
929
  }
875
930
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
876
931
  const upload = deps.uploadApp ?? (async (f, n) => {
877
- const { uploadAppBuild } = await import("./upload-app-UHJLFQKZ.js");
932
+ const { uploadAppBuild } = await import("./upload-app-ZEQMGZIT.js");
878
933
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
879
934
  return result2;
880
935
  });
@@ -1270,6 +1325,23 @@ var MCP_INSTRUCTIONS = [
1270
1325
  "A session stopped elsewhere is reported as ended; call dev_start again for a new one.",
1271
1326
  'A session nobody uses ends after the idle timeout. A result that ends with "Session \u2026 ends in N s for inactivity" means act (tap, type, swipe, press) to keep it, or dev_stop if you are done; ui_tree, screenshot, logs, find and wait do not count as use.'
1272
1327
  ].join("\n");
1328
+ function toolInputSchema(def, z) {
1329
+ const shape = {};
1330
+ for (const [key, prop] of Object.entries(def.properties)) {
1331
+ const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
1332
+ const described = base.describe(prop.description);
1333
+ shape[key] = def.required.includes(key) ? described : described.optional();
1334
+ }
1335
+ const valid = Object.keys(def.properties);
1336
+ return z.strictObject(shape, {
1337
+ error: (issue) => {
1338
+ if (issue.code !== "unrecognized_keys") return void 0;
1339
+ const unknown = issue.keys.map((k) => `"${k}"`).join(", ");
1340
+ const takes = valid.length ? `${def.name} takes: ${valid.join(", ")}.` : `${def.name} takes no arguments.`;
1341
+ return `Unknown argument${issue.keys.length === 1 ? "" : "s"} ${unknown}. ${takes}`;
1342
+ }
1343
+ });
1344
+ }
1273
1345
  async function createMcpServer(opts) {
1274
1346
  const [{ McpServer }, { z }] = await Promise.all([
1275
1347
  import("@modelcontextprotocol/sdk/server/mcp.js"),
@@ -1288,21 +1360,12 @@ async function createMcpServer(opts) {
1288
1360
  { name: "vibeview", version: opts.version ?? "0.0.0" },
1289
1361
  { instructions: MCP_INSTRUCTIONS }
1290
1362
  );
1291
- function zodShape(def) {
1292
- const shape = {};
1293
- for (const [key, prop] of Object.entries(def.properties)) {
1294
- const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
1295
- const described = base.describe(prop.description);
1296
- shape[key] = def.required.includes(key) ? described : described.optional();
1297
- }
1298
- return shape;
1299
- }
1300
1363
  for (const def of buildToolDefinitions()) {
1301
1364
  server.registerTool(
1302
1365
  def.name,
1303
1366
  {
1304
1367
  description: def.description,
1305
- inputSchema: zodShape(def),
1368
+ inputSchema: toolInputSchema(def, z),
1306
1369
  annotations: { readOnlyHint: !def.mutating }
1307
1370
  },
1308
1371
  async (args, extra) => {
@@ -1389,7 +1452,10 @@ export {
1389
1452
  idleNoticeText,
1390
1453
  mcpText,
1391
1454
  progressReporter,
1455
+ runDestination,
1392
1456
  runMcpServer,
1393
1457
  startProgress,
1394
- startedDevice
1458
+ startedDevice,
1459
+ testToolErrorMessage,
1460
+ toolInputSchema
1395
1461
  };
@@ -3,7 +3,7 @@ import {
3
3
  describeBuild,
4
4
  uploadApp,
5
5
  uploadAppBuild
6
- } from "./chunk-MYXUGCR5.js";
6
+ } from "./chunk-GDTLLJFL.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.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",