vibeview 0.37.1 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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-NJ2IXB5H.js";
30
+ runAgentVerb,
31
+ runStepsNotChecked,
32
+ stepsNotCheckedLabel
33
+ } from "./chunk-EQN3X62U.js";
29
34
  import {
30
35
  ApiClient,
31
36
  ApiStatusError,
@@ -46,7 +51,7 @@ import {
46
51
  resolveAuth,
47
52
  saveConfig,
48
53
  uploadApp
49
- } from "./chunk-QRJAFBH3.js";
54
+ } from "./chunk-MYXUGCR5.js";
50
55
 
51
56
  // src/cli.ts
52
57
  import { createRequire } from "module";
@@ -61,21 +66,6 @@ import chalk2 from "chalk";
61
66
  // src/lib/formatters/human.ts
62
67
  import chalk from "chalk";
63
68
 
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
69
  // src/lib/formatters/cases.ts
80
70
  function resultCases(result) {
81
71
  if (Array.isArray(result.test_runs)) return result.test_runs;
@@ -90,6 +80,9 @@ function resultCases(result) {
90
80
  error_kind_description: failedStep?.error_kind_description ?? null,
91
81
  screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
92
82
  url: result.url,
83
+ quarantined: result.quarantined,
84
+ not_checked: result.not_checked,
85
+ not_run: result.not_run,
93
86
  steps_not_checked: runStepsNotChecked(result),
94
87
  not_checked_steps: notCheckedSteps(result.steps).map((s) => ({
95
88
  step: s.step,
@@ -117,19 +110,21 @@ function failureLines(failure) {
117
110
  }
118
111
  function formatHuman(result, verbose) {
119
112
  const lines = [];
120
- const icon = result.status === "passed" ? chalk.green("PASS") : chalk.red("FAIL");
113
+ const outcome = outcomeOf(result);
114
+ 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");
121
115
  const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
122
116
  lines.push("");
123
117
  const title = result.suite_name || result.case_name || result.run_id;
124
118
  const cases = resultCases(result);
125
119
  const notChecked = cases.reduce((n, t) => n + (t.steps_not_checked ?? 0), 0);
126
- const runSuffix = notCheckedSuffix(result.status, notChecked);
120
+ const runSuffix = notCheckedSuffix(outcome, notChecked);
127
121
  lines.push(
128
122
  ` ${icon}${runSuffix ? chalk.yellow(runSuffix) : ""} ${chalk.bold(title)} ${chalk.dim(duration)}`
129
123
  );
130
- const passed = cases.filter((t) => t.status === "passed").length;
124
+ const passed = cases.filter((t) => outcomeOf(t) === "passed").length;
125
+ const notCheckedCases = cases.filter((t) => outcomeOf(t) === "not_checked").length;
131
126
  const total = cases.length;
132
- const notCheckedNote = notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
127
+ const notCheckedNote = notCheckedCases > 0 ? `, ${notCheckedCases} not checked` : notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
133
128
  lines.push(` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}`)}`);
134
129
  if (result.url) {
135
130
  lines.push(` ${chalk.dim(result.url)}`);
@@ -138,16 +133,19 @@ function formatHuman(result, verbose) {
138
133
  if (suiteCases.length > 0) {
139
134
  lines.push("");
140
135
  for (const tr of suiteCases) {
141
- const caseIcon = tr.status === "passed" ? chalk.green(" PASS") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
136
+ const caseOutcome = outcomeOf(tr);
137
+ 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");
142
138
  const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
143
- const caseSuffix = notCheckedSuffix(tr.status, tr.steps_not_checked);
139
+ const caseSuffix = notCheckedSuffix(caseOutcome, tr.steps_not_checked);
144
140
  lines.push(
145
141
  ` ${caseIcon}${caseSuffix ? chalk.yellow(caseSuffix) : ""} ${tr.case_name} ${chalk.dim(caseDuration)}`
146
142
  );
147
143
  if (tr.url) {
148
144
  lines.push(` ${chalk.dim(tr.url)}`);
149
145
  }
150
- if (tr.status !== "passed") {
146
+ if (caseOutcome === "not_checked" || caseOutcome === "not_run") {
147
+ if (tr.error_message) lines.push(` ${chalk.dim(tr.error_message)}`);
148
+ } else if (tr.status !== "passed") {
151
149
  lines.push(...failureLines(tr));
152
150
  }
153
151
  }
@@ -173,7 +171,10 @@ function formatHuman(result, verbose) {
173
171
  }
174
172
  }
175
173
  }
176
- if (result.error_message) {
174
+ if (result.error_message && outcome === "not_checked") {
175
+ lines.push(`
176
+ ${chalk.dim(result.error_message)}`);
177
+ } else if (result.error_message) {
177
178
  lines.push(`
178
179
  ${chalk.red("Error:")} ${result.error_message}`);
179
180
  }
@@ -214,21 +215,29 @@ function caseDetailLines(c) {
214
215
  if (out.length) lines.push(` <system-out>${escapeXml(out.join("\n"))}</system-out>`);
215
216
  return lines;
216
217
  }
218
+ var isQuarantinedFailure = (t) => t.status === "failed" && t.quarantined === true;
219
+ var NEUTRAL = /* @__PURE__ */ new Set(["not_checked", "not_run"]);
220
+ function neutralMessage(outcome, message) {
221
+ if (outcome === "not_checked") return `not checked: ${message || "no step was checked"}`;
222
+ return message || "Not run";
223
+ }
217
224
  function formatJunit(result) {
218
225
  const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
219
226
  const cases = resultCases(result);
220
227
  const tests = cases.length;
221
- const failures = cases.filter((t) => t.status === "failed").length;
228
+ const failures = cases.filter((t) => t.status === "failed" && !isQuarantinedFailure(t)).length;
222
229
  const errors = cases.filter((t) => t.status === "error").length;
230
+ const skipped = cases.filter((t) => isQuarantinedFailure(t) || NEUTRAL.has(outcomeOf(t))).length;
231
+ const skippedAttr = skipped > 0 ? ` skipped="${skipped}"` : "";
223
232
  const totalTime = result.duration_ms ? (result.duration_ms / 1e3).toFixed(1) : "0";
224
233
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
225
234
  const lines = [];
226
235
  lines.push('<?xml version="1.0" encoding="UTF-8"?>');
227
236
  lines.push(
228
- `<testsuites name="vibeview" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}">`
237
+ `<testsuites name="vibeview" tests="${tests}" failures="${failures}" errors="${errors}"${skippedAttr} time="${totalTime}">`
229
238
  );
230
239
  lines.push(
231
- ` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}" timestamp="${timestamp}">`
240
+ ` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}"${skippedAttr} time="${totalTime}" timestamp="${timestamp}">`
232
241
  );
233
242
  if (result.url || result.build_warning) {
234
243
  lines.push(" <properties>");
@@ -245,13 +254,21 @@ function formatJunit(result) {
245
254
  for (const tr of cases) {
246
255
  const caseName = escapeXml(tr.case_name);
247
256
  const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
248
- const detail = caseDetailLines(tr);
249
- if (tr.status === "passed" && detail.length === 0) {
257
+ const outcome = outcomeOf(tr);
258
+ const detail = caseDetailLines({ ...tr, status: outcome });
259
+ if (outcome === "passed" && detail.length === 0) {
250
260
  lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
251
261
  continue;
252
262
  }
253
263
  lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
254
- if (tr.status === "failed") {
264
+ if (isQuarantinedFailure(tr)) {
265
+ const msg = escapeXml(tr.error_message || "Test failed");
266
+ lines.push(` <skipped message="quarantined: ${msg}" />`);
267
+ } else if (NEUTRAL.has(outcome)) {
268
+ lines.push(
269
+ ` <skipped message="${escapeXml(neutralMessage(outcome, tr.error_message))}" />`
270
+ );
271
+ } else if (tr.status === "failed") {
255
272
  const msg = escapeXml(tr.error_message || "Test failed");
256
273
  lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
257
274
  } else if (tr.status !== "passed") {
@@ -287,7 +304,7 @@ function formatJunitError(name, message, url) {
287
304
  lines.push("</testsuites>");
288
305
  return lines.join("\n");
289
306
  }
290
- var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
307
+ var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
291
308
  function caseLines(c, classname) {
292
309
  const name = escapeXml(c.name);
293
310
  const head = ` <testcase name="${name}" classname="${classname}" time="${c.time}"`;
@@ -324,6 +341,21 @@ function formatMultiSuiteJunit(result, opts = {}) {
324
341
  url: c.url || void 0,
325
342
  steps_not_checked: c.steps_not_checked
326
343
  };
344
+ const outcome = outcomeOf(c);
345
+ if (NEUTRAL.has(outcome)) {
346
+ return {
347
+ ...base,
348
+ verdict: "skipped",
349
+ message: neutralMessage(outcome, c.error_message)
350
+ };
351
+ }
352
+ if (c.status === "failed" && c.quarantined === true) {
353
+ return {
354
+ ...base,
355
+ verdict: "skipped",
356
+ message: `quarantined: ${c.error_message || "Test failed"}`
357
+ };
358
+ }
327
359
  switch (c.status) {
328
360
  case "passed":
329
361
  return { ...base, verdict: "passed" };
@@ -395,6 +427,7 @@ function formatMultiSuiteJunit(result, opts = {}) {
395
427
  }
396
428
 
397
429
  // src/lib/poll.ts
430
+ var TERMINAL_STATUSES = ["passed", "failed", "error", "cancelled", "not_checked"];
398
431
  function resumeSpinner(spinner, text) {
399
432
  if (spinner.isEnabled === false) {
400
433
  if (text !== void 0) spinner.text = text;
@@ -492,20 +525,26 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
492
525
  `${chalk2.cyan("\u25B6")} ${chalk2.bold(tr.case_name || "Case")} started`
493
526
  );
494
527
  }
495
- const isTerminal = ["passed", "failed", "error", "cancelled"].includes(cur);
496
- const wasTerminal = prev && ["passed", "failed", "error", "cancelled"].includes(prev);
528
+ const isTerminal = TERMINAL_STATUSES.includes(cur);
529
+ const wasTerminal = prev && TERMINAL_STATUSES.includes(prev);
497
530
  if (isTerminal && !wasTerminal) {
498
- const icon = cur === "passed" ? chalk2.green("\u2713") : cur === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
531
+ const outcome = outcomeOf(tr);
532
+ const neutral = outcome === "not_checked" || outcome === "not_run";
533
+ const icon = neutral ? chalk2.gray("\u25CB") : cur === "passed" ? chalk2.green("\u2713") : cur === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
534
+ const label = neutral ? outcome.replace("_", " ") : cur;
499
535
  const dur = tr.duration_ms ? ` (${(tr.duration_ms / 1e3).toFixed(1)}s)` : "";
500
- printBelowSpinner(spinner, `${icon} ${chalk2.bold(tr.case_name || "Case")} ${cur}${dur}`);
501
- if (tr.error_message && cur !== "passed") {
536
+ printBelowSpinner(
537
+ spinner,
538
+ `${icon} ${chalk2.bold(tr.case_name || "Case")} ${label}${dur}`
539
+ );
540
+ if (tr.error_message && cur !== "passed" && !neutral) {
502
541
  printBelowSpinner(spinner, ` ${chalk2.red(tr.error_message)}`);
503
542
  }
504
543
  }
505
544
  caseStateBefore.set(tr.public_id, cur);
506
545
  }
507
546
  }
508
- if (["passed", "failed", "error", "cancelled"].includes(result.status)) {
547
+ if (TERMINAL_STATUSES.includes(result.status)) {
509
548
  if (printedSteps.size > 0 && Array.isArray(result.steps)) {
510
549
  spinner.stop();
511
550
  result.steps.forEach((st, i) => {
@@ -555,7 +594,7 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
555
594
  if (result.type === "suite_run" && Array.isArray(result.test_runs)) {
556
595
  const total = result.total_cases ?? result.test_runs.length;
557
596
  const done = result.test_runs.filter(
558
- (tr) => ["passed", "failed", "error", "cancelled"].includes(tr.status ?? "")
597
+ (tr) => TERMINAL_STATUSES.includes(tr.status ?? "")
559
598
  ).length;
560
599
  spinner.text = `Running suite \u2014 ${done}/${total} cases complete...`;
561
600
  } else {
@@ -652,10 +691,10 @@ function displayProgressEvent(evt, ctx) {
652
691
  break;
653
692
  }
654
693
  case "run_completed": {
655
- const icon = evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
694
+ const icon = evt.status === "not_checked" ? chalk2.gray("\u25CB") : evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
656
695
  spinner.stop();
657
696
  process.stderr.write(
658
- `${icon} Run ${evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
697
+ `${icon} Run ${evt.status === "not_checked" ? "not checked" : evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
659
698
  `
660
699
  );
661
700
  resumeSpinner(spinner);
@@ -695,14 +734,30 @@ function outputResult(result, options) {
695
734
  }
696
735
  }
697
736
  function mapExitCode(status) {
698
- if (status === "passed") return EXIT_PASS;
737
+ if (status === "passed" || status === "not_checked") return EXIT_PASS;
699
738
  if (status === "failed") return EXIT_TEST_FAIL;
700
739
  return EXIT_INFRA_ERROR;
701
740
  }
741
+ function isQuarantinedFailure2(result) {
742
+ return result.status === "failed" && result.quarantined === true;
743
+ }
744
+ function runExitCode(result) {
745
+ return isQuarantinedFailure2(result) ? EXIT_PASS : mapExitCode(result.status);
746
+ }
702
747
 
703
748
  // src/lib/run-target.ts
704
749
  import chalk3 from "chalk";
705
750
  var RUN_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
751
+ var SUITE_FILTER_PLATFORMS = [...RUN_PLATFORMS, "roku"];
752
+ function parseSuitePlatformFilter(value) {
753
+ if (value === void 0) return void 0;
754
+ if (!SUITE_FILTER_PLATFORMS.includes(value)) {
755
+ throw new Error(
756
+ `--platform must be one of ${SUITE_FILTER_PLATFORMS.join(", ")}, got "${value}"`
757
+ );
758
+ }
759
+ return value;
760
+ }
706
761
  function parseRunPlatform(value) {
707
762
  if (value === void 0) return void 0;
708
763
  if (!RUN_PLATFORMS.includes(value)) {
@@ -802,6 +857,8 @@ ${signal} received.
802
857
  device: options.device,
803
858
  build: options.build,
804
859
  metadata: options.metadata,
860
+ // Opt-in: always sent, so an older server's CI default never retries.
861
+ retryFailed: options.retry === true,
805
862
  platform: options.platform,
806
863
  app: options.app
807
864
  });
@@ -838,7 +895,7 @@ ${signal} received.
838
895
  response.build_warning ? { ...result, build_warning: response.build_warning } : result,
839
896
  options
840
897
  );
841
- process.exit(mapExitCode(result.status));
898
+ process.exit(runExitCode(result));
842
899
  } catch (err) {
843
900
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
844
901
  outputRunError(err, options, {
@@ -928,7 +985,9 @@ ${signal} received.
928
985
  device_category: opts.device ? void 0 : opts.deviceCategory,
929
986
  device_id: opts.device,
930
987
  build_id: opts.build,
931
- metadata: opts.metadata
988
+ metadata: opts.metadata,
989
+ // Opt-in: always sent, so an older server's CI default never retries.
990
+ retry_failed: opts.retry === true
932
991
  });
933
992
  } catch (err) {
934
993
  spinner.stop();
@@ -989,7 +1048,7 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
989
1048
  pending = [];
990
1049
  if (nextText !== null) spinner.start(nextText);
991
1050
  };
992
- const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
1051
+ const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
993
1052
  let last = null;
994
1053
  const abandon = async (reason) => {
995
1054
  flush(null);
@@ -1048,10 +1107,11 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
1048
1107
  }
1049
1108
  const wasTerminal = prev !== void 0 && TERMINAL.has(prev);
1050
1109
  if (TERMINAL.has(cur) && !wasTerminal) {
1051
- const mark = cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
1110
+ const nothingChecked = outcomeOf(sr) === "not_checked";
1111
+ const mark = nothingChecked ? chalk5.gray("\u25CB") : cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
1052
1112
  const link = sr.url ? chalk5.dim(" " + sr.url) : "";
1053
1113
  print(
1054
- `${mark} ${chalk5.bold(sr.suite_name)} ${cur}: ${sr.passed} passed, ${sr.failed} failed${link}`
1114
+ `${mark} ${chalk5.bold(sr.suite_name)} ${nothingChecked ? "not checked" : cur}: ${suiteCounts(sr)}${link}`
1055
1115
  );
1056
1116
  if (sr.error_message) {
1057
1117
  print(` ${chalk5.red(sr.error_message)}`);
@@ -1105,39 +1165,48 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
1105
1165
  0
1106
1166
  );
1107
1167
  const aggregateNote = totalNotChecked > 0 ? chalk5.yellow(` (${stepsNotCheckedLabel(totalNotChecked)})`) : "";
1168
+ const aggregate = outcomeOf(last) === "not_checked" ? "not checked" : last.status;
1108
1169
  process.stdout.write(`
1109
- Aggregate: ${last.status}${aggregateNote}
1170
+ Aggregate: ${aggregate}${aggregateNote}
1110
1171
  `);
1111
1172
  for (const sr of last.suite_runs) {
1112
- const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
1173
+ const mark = outcomeOf(sr) === "not_checked" ? chalk5.gray("\u25CB") : sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
1113
1174
  const link = sr.url ? chalk5.dim(` ${sr.url}`) : "";
1114
- process.stdout.write(
1115
- ` ${mark} ${sr.suite_name}: ${sr.passed} passed, ${sr.failed} failed${link}
1116
- `
1117
- );
1175
+ process.stdout.write(` ${mark} ${sr.suite_name}: ${suiteCounts(sr)}${link}
1176
+ `);
1118
1177
  if (sr.error_message) {
1119
1178
  process.stdout.write(` ${chalk5.red(sr.error_message)}
1120
1179
  `);
1121
1180
  }
1122
1181
  for (const cr of sr.case_runs ?? []) {
1123
- const caseMark = cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
1182
+ const outcome = outcomeOf(cr);
1183
+ const neutral = outcome === "not_checked" || outcome === "not_run";
1184
+ const caseMark = neutral ? chalk5.gray("\u25CB") : cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
1124
1185
  const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
1125
- const unchecked = notCheckedSuffix(cr.status, cr.steps_not_checked);
1186
+ const unchecked = notCheckedSuffix(outcome, cr.steps_not_checked);
1187
+ const label = neutral ? outcome.replace("_", " ") : cr.status ?? "?";
1126
1188
  process.stdout.write(
1127
- ` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
1189
+ ` ${caseMark} ${cr.case_name ?? "Case"} ${label}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
1128
1190
  `
1129
1191
  );
1130
- if (cr.error_message && cr.status !== "passed") {
1192
+ if (cr.error_message && neutral) {
1193
+ process.stdout.write(` ${chalk5.dim(cr.error_message)}
1194
+ `);
1195
+ } else if (cr.error_message && cr.status !== "passed") {
1131
1196
  process.stdout.write(` ${chalk5.red(cr.error_message)}
1132
1197
  `);
1133
1198
  }
1134
1199
  }
1135
1200
  }
1136
1201
  }
1137
- if (last.status === "passed") process.exit(EXIT_PASS);
1202
+ if (last.status === "passed" || last.status === "not_checked") process.exit(EXIT_PASS);
1138
1203
  if (last.status === "failed") process.exit(EXIT_TEST_FAIL);
1139
1204
  process.exit(EXIT_INFRA_ERROR);
1140
1205
  }
1206
+ function suiteCounts(sr) {
1207
+ const nc = sr.not_checked_tests ?? 0;
1208
+ return `${sr.passed} passed, ${sr.failed} failed${nc > 0 ? `, ${nc} not checked` : ""}`;
1209
+ }
1141
1210
 
1142
1211
  // src/commands/run-test.ts
1143
1212
  import chalk6 from "chalk";
@@ -1206,6 +1275,8 @@ ${signal} received.
1206
1275
  device: options.device,
1207
1276
  build: options.build,
1208
1277
  metadata: options.metadata,
1278
+ // Opt-in: always sent, so an older server's CI default never retries.
1279
+ retryFailed: options.retry === true,
1209
1280
  platform: options.platform,
1210
1281
  app: options.app
1211
1282
  });
@@ -1243,7 +1314,7 @@ ${chalk6.dim("Test:")} ${testId}
1243
1314
  response.build_warning ? { ...result, build_warning: response.build_warning } : result,
1244
1315
  options
1245
1316
  );
1246
- process.exit(mapExitCode(result.status));
1317
+ process.exit(runExitCode(result));
1247
1318
  } catch (err) {
1248
1319
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
1249
1320
  outputRunError(err, options, {
@@ -1383,11 +1454,11 @@ async function stopCommand(sessionId, opts, deps) {
1383
1454
  import chalk10 from "chalk";
1384
1455
  import { existsSync, readFileSync } from "fs";
1385
1456
  async function readStdin() {
1386
- return new Promise((resolve, reject) => {
1457
+ return new Promise((resolve2, reject) => {
1387
1458
  let data = "";
1388
1459
  const timeout = setTimeout(() => {
1389
1460
  process.stdin.removeAllListeners();
1390
- resolve(data);
1461
+ resolve2(data);
1391
1462
  }, 5e3);
1392
1463
  process.stdin.setEncoding("utf-8");
1393
1464
  process.stdin.on("data", (chunk) => {
@@ -1395,7 +1466,7 @@ async function readStdin() {
1395
1466
  });
1396
1467
  process.stdin.on("end", () => {
1397
1468
  clearTimeout(timeout);
1398
- resolve(data);
1469
+ resolve2(data);
1399
1470
  });
1400
1471
  process.stdin.on("error", (err) => {
1401
1472
  clearTimeout(timeout);
@@ -1439,14 +1510,16 @@ function assertHasSteps(steps) {
1439
1510
  async function resolveAppId(client, appRef, platform) {
1440
1511
  if (appRef.includes(".")) {
1441
1512
  if (!platform) {
1442
- throw new Error(
1513
+ throw new UsageError(
1443
1514
  "When using a package name for --app, you must also specify --platform (ios, android)."
1444
1515
  );
1445
1516
  }
1446
1517
  const apps = await client.listApps();
1447
1518
  const match = apps.find((a) => a.package_name === appRef && a.platform === platform);
1448
1519
  if (!match) {
1449
- throw new Error(`No app found with package name "${appRef}" and platform "${platform}".`);
1520
+ throw new UsageError(
1521
+ `No app found with package name "${appRef}" and platform "${platform}".`
1522
+ );
1450
1523
  }
1451
1524
  return match.public_id;
1452
1525
  }
@@ -1470,7 +1543,8 @@ async function createTest(options) {
1470
1543
  steps,
1471
1544
  context: options.context,
1472
1545
  mode: "ai",
1473
- if_exists: options.ifExists
1546
+ if_exists: options.ifExists,
1547
+ group_ids: options.group?.length ? options.group : void 0
1474
1548
  });
1475
1549
  spinner.succeed(
1476
1550
  result.outcome === "updated" ? `Test updated: ${result.name}` : result.outcome === "unchanged" ? `Test already exists, unchanged: ${result.name}` : `Test created: ${result.name}`
@@ -1483,6 +1557,7 @@ async function createTest(options) {
1483
1557
  name: result.name,
1484
1558
  app_id: appPublicId || null,
1485
1559
  outcome: result.outcome ?? null,
1560
+ groups: result.groups ?? [],
1486
1561
  url: testCaseUrl(baseUrl, result.public_id)
1487
1562
  },
1488
1563
  null,
@@ -1498,6 +1573,9 @@ async function createTest(options) {
1498
1573
  if (result.platform) {
1499
1574
  console.log(` ${chalk10.dim(`Platform: ${result.platform}`)}`);
1500
1575
  }
1576
+ if (result.groups?.length) {
1577
+ console.log(` ${chalk10.dim(`Groups: ${result.groups.map((g) => g.name).join(", ")}`)}`);
1578
+ }
1501
1579
  console.log(` ${chalk10.dim(`Steps: ${steps.length}`)}`);
1502
1580
  console.log(chalk10.dim(` ${testCaseUrl(baseUrl, result.public_id)}
1503
1581
  `));
@@ -1511,6 +1589,153 @@ async function createTest(options) {
1511
1589
  }
1512
1590
  }
1513
1591
 
1592
+ // src/commands/import-maestro.ts
1593
+ import chalk11 from "chalk";
1594
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
1595
+ import { basename, extname, join, relative, resolve, sep } from "path";
1596
+ import { zipSync } from "fflate";
1597
+ var MAX_FOLDER_FLOWS = 200;
1598
+ var MAX_FLOW_BYTES = 256 * 1024;
1599
+ var MAX_TOTAL_BYTES = 5 * 1024 * 1024;
1600
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
1601
+ function readMaestroUpload(path) {
1602
+ if (!existsSync2(path)) throw new UsageError(`Not found: ${path}`);
1603
+ if (statSync(path).isFile()) {
1604
+ const ext = extname(path).toLowerCase();
1605
+ if (![".yaml", ".yml", ".zip"].includes(ext)) {
1606
+ throw new UsageError(
1607
+ "Pass a Maestro flow (.yaml or .yml), a .zip of your flows folder, or the folder itself."
1608
+ );
1609
+ }
1610
+ const size = statSync(path).size;
1611
+ const cap = ext === ".zip" ? MAX_TOTAL_BYTES : MAX_FLOW_BYTES;
1612
+ if (size > cap) {
1613
+ throw new UsageError(`${path} is larger than ${cap / 1024} KB.`);
1614
+ }
1615
+ return { name: basename(path), bytes: readFileSync2(path) };
1616
+ }
1617
+ const root = resolve(path);
1618
+ const entries = {};
1619
+ let count = 0;
1620
+ let total = 0;
1621
+ const walk = (dir) => {
1622
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
1623
+ if (ent.isSymbolicLink()) continue;
1624
+ const full = join(dir, ent.name);
1625
+ if (ent.isDirectory()) {
1626
+ if (!SKIP_DIRS.has(ent.name)) walk(full);
1627
+ continue;
1628
+ }
1629
+ if (!ent.isFile() || !/\.ya?ml$/i.test(ent.name)) continue;
1630
+ const rel = relative(root, full).split(sep).join("/");
1631
+ const bytes2 = readFileSync2(full);
1632
+ count += 1;
1633
+ total += bytes2.length;
1634
+ if (count > MAX_FOLDER_FLOWS) {
1635
+ throw new UsageError(`${path} holds more than ${MAX_FOLDER_FLOWS} flow files.`);
1636
+ }
1637
+ if (bytes2.length > MAX_FLOW_BYTES) {
1638
+ throw new UsageError(`${rel} is larger than ${MAX_FLOW_BYTES / 1024} KB.`);
1639
+ }
1640
+ if (total > MAX_TOTAL_BYTES) {
1641
+ throw new UsageError(`The flows in ${path} add up to more than 5 MB.`);
1642
+ }
1643
+ entries[rel] = bytes2;
1644
+ }
1645
+ };
1646
+ walk(root);
1647
+ if (count === 0) throw new UsageError(`No .yaml or .yml flows found in ${path}.`);
1648
+ const bytes = zipSync(entries);
1649
+ if (bytes.length > MAX_TOTAL_BYTES) {
1650
+ throw new UsageError(`The zipped flows in ${path} are larger than 5 MB.`);
1651
+ }
1652
+ return { name: `${basename(root) || "flows"}.zip`, bytes };
1653
+ }
1654
+ var FLAG_LABEL = {
1655
+ rewrite: "needs rewrite",
1656
+ check: "check",
1657
+ dropped: "not imported"
1658
+ };
1659
+ var START_STATE_LABEL = {
1660
+ reinstall: "Fresh install",
1661
+ clear_data: "Clean app data",
1662
+ relaunch: "Restart the app",
1663
+ none: "Where the last test stopped"
1664
+ };
1665
+ function flagLine(f) {
1666
+ const where = [f.file, f.where, f.command].filter(Boolean).join(" \xB7 ");
1667
+ const label = f.severity === "rewrite" ? chalk11.red(FLAG_LABEL.rewrite) : FLAG_LABEL[f.severity];
1668
+ return ` ${label} ${chalk11.dim(where)} ${f.message}`;
1669
+ }
1670
+ function formatImport(result, baseUrl) {
1671
+ const lines = [];
1672
+ const tests = result.tests.filter((t) => t.importable);
1673
+ if (result.created === null) {
1674
+ lines.push(
1675
+ `Preview: ${tests.length} test(s) and ${result.setups.length} setup(s) would be created. Nothing was saved; run again without --dry-run to import.`
1676
+ );
1677
+ } else {
1678
+ lines.push(`Created ${result.created} test(s), setups included.`);
1679
+ }
1680
+ for (const f of result.flags) lines.push(flagLine(f));
1681
+ for (const t of [...result.tests, ...result.setups]) {
1682
+ const meta = [`${t.steps.length} ${t.steps.length === 1 ? "step" : "steps"}`];
1683
+ if (t.setup_keys.length) meta.push(`before this test: ${t.setup_keys.join(", ")}`);
1684
+ if (t.start_state) meta.push(`start: ${START_STATE_LABEL[t.start_state] ?? t.start_state}`);
1685
+ const head = t.importable ? t.name : chalk11.red(`${t.name} (not imported)`);
1686
+ lines.push("", ` ${head} ${chalk11.dim(`${t.key} \xB7 ${meta.join(" \xB7 ")}`)}`);
1687
+ if (t.created_id) lines.push(` ${chalk11.dim(testCaseUrl(baseUrl, t.created_id))}`);
1688
+ for (const f of t.flags) lines.push(flagLine(f));
1689
+ }
1690
+ if (result.test_data.length) {
1691
+ lines.push("", "These tests use test data:");
1692
+ for (const d of result.test_data) {
1693
+ const note = d.status === "needs_value" || d.status === "skipped" || d.suggested_value === null ? "add the value under App notes & data" : d.status === "kept" ? "already set, kept" : d.status === "created" ? "added from the flow" : d.secret ? "stored as a secret" : `flow value: ${d.suggested_value}`;
1694
+ lines.push(` ${d.key} ${chalk11.dim(note)}`);
1695
+ }
1696
+ }
1697
+ return lines;
1698
+ }
1699
+ async function importMaestro(path, options) {
1700
+ let upload;
1701
+ try {
1702
+ if (!options.app) throw new UsageError("Pass the app to import into: --app <app id>.");
1703
+ upload = readMaestroUpload(path);
1704
+ } catch (err) {
1705
+ if (options.output === "json") printJsonError(err);
1706
+ throw err;
1707
+ }
1708
+ const { token, baseUrl } = resolveAuth(options);
1709
+ const client = new ApiClient(baseUrl, token);
1710
+ try {
1711
+ const appId = await resolveAppId(client, options.app, options.platform);
1712
+ const spinner = createSpinner(options.dryRun ? "Reading flows..." : "Importing flows...", {
1713
+ silent: options.output === "json"
1714
+ });
1715
+ spinner.start();
1716
+ let result;
1717
+ try {
1718
+ result = await client.importMaestro(upload, {
1719
+ appId,
1720
+ group: options.group,
1721
+ dryRun: options.dryRun === true
1722
+ });
1723
+ } finally {
1724
+ spinner.stop();
1725
+ }
1726
+ if (options.output === "json") {
1727
+ console.log(JSON.stringify(result, null, 2));
1728
+ return;
1729
+ }
1730
+ for (const line of formatImport(result, baseUrl)) console.log(line);
1731
+ } catch (err) {
1732
+ if (options.output === "json") printJsonError(err);
1733
+ console.error(chalk11.red(err instanceof Error ? err.message : "Import failed"));
1734
+ const rejected = err instanceof UsageError || err instanceof NotFoundError || err instanceof ApiStatusError && err.status >= 400 && err.status < 500 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR;
1735
+ process.exit(errorExitCode(err, rejected));
1736
+ }
1737
+ }
1738
+
1514
1739
  // src/commands/login.ts
1515
1740
  import { randomUUID } from "crypto";
1516
1741
  import open from "open";
@@ -1588,7 +1813,7 @@ async function logout() {
1588
1813
  }
1589
1814
 
1590
1815
  // src/commands/dev-status.ts
1591
- import chalk11 from "chalk";
1816
+ import chalk12 from "chalk";
1592
1817
  function buildLine(info) {
1593
1818
  if (!info || typeof info.app_build_id !== "number") return null;
1594
1819
  const version = info.app_version ? ` version ${info.app_version}${info.app_build_number ? ` (${info.app_build_number})` : ""}` : "";
@@ -1649,7 +1874,7 @@ async function devStatus(opts) {
1649
1874
  console.error(` url: ${state.page_url}`);
1650
1875
  console.error(` started: ${state.started_at}`);
1651
1876
  }
1652
- if (metro) console.error(chalk11.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
1877
+ if (metro) console.error(chalk12.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
1653
1878
  process.exit(0);
1654
1879
  }
1655
1880
  var LIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "queued", "starting", "active"]);
@@ -1674,7 +1899,7 @@ async function reportOrphan(api, state, opts) {
1674
1899
  );
1675
1900
  } else if (live) {
1676
1901
  console.error(
1677
- chalk11.yellow(
1902
+ chalk12.yellow(
1678
1903
  `\u26A0 The dev process (pid ${state.pid}) has exited, but session ${state.session_id} is still ${status}.`
1679
1904
  )
1680
1905
  );
@@ -1722,12 +1947,12 @@ async function devStop(opts) {
1722
1947
  }
1723
1948
 
1724
1949
  // src/commands/dev-reload.ts
1725
- import chalk12 from "chalk";
1950
+ import chalk13 from "chalk";
1726
1951
  function fail(opts, code, message, exit) {
1727
1952
  if (opts.json) {
1728
1953
  console.log(JSON.stringify({ reloaded: false, code, message }));
1729
1954
  } else {
1730
- console.error(chalk12.red(`error: ${message}`));
1955
+ console.error(chalk13.red(`error: ${message}`));
1731
1956
  }
1732
1957
  process.exit(exit);
1733
1958
  }
@@ -1780,7 +2005,7 @@ async function devReload(opts) {
1780
2005
  api,
1781
2006
  // Build output and the "reloaded in" line are human output: stderr, so
1782
2007
  // stdout stays reserved for the --json result.
1783
- log: (line) => console.error(chalk12.dim(line))
2008
+ log: (line) => console.error(chalk13.dim(line))
1784
2009
  });
1785
2010
  const started = Date.now();
1786
2011
  try {
@@ -1802,10 +2027,10 @@ async function devReload(opts) {
1802
2027
  }
1803
2028
 
1804
2029
  // src/commands/builds.ts
1805
- import chalk14 from "chalk";
2030
+ import chalk15 from "chalk";
1806
2031
 
1807
2032
  // src/lib/formatters/builds.ts
1808
- import chalk13 from "chalk";
2033
+ import chalk14 from "chalk";
1809
2034
  var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
1810
2035
  var UNBILLED_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
1811
2036
  function formatCostCell(costMicro, status) {
@@ -1813,13 +2038,13 @@ function formatCostCell(costMicro, status) {
1813
2038
  return `$${(Math.max(costMicro, 0) / 1e6).toFixed(2)}`;
1814
2039
  }
1815
2040
  var STATUS_COLOR = {
1816
- succeeded: chalk13.green,
1817
- failed: chalk13.red,
1818
- timed_out: chalk13.red,
1819
- infra_failed: chalk13.red,
1820
- canceled: chalk13.dim,
1821
- running: chalk13.yellow,
1822
- queued: chalk13.dim
2041
+ succeeded: chalk14.green,
2042
+ failed: chalk14.red,
2043
+ timed_out: chalk14.red,
2044
+ infra_failed: chalk14.red,
2045
+ canceled: chalk14.dim,
2046
+ running: chalk14.yellow,
2047
+ queued: chalk14.dim
1823
2048
  };
1824
2049
  function formatBuildsHuman(builds2) {
1825
2050
  if (builds2.length === 0) {
@@ -1835,13 +2060,13 @@ function formatBuildsHuman(builds2) {
1835
2060
  ]);
1836
2061
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1837
2062
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1838
- const join4 = (cells) => cells.join(" ").trimEnd();
1839
- const lines = [chalk13.bold(join4(padCells(HEADERS)))];
2063
+ const join5 = (cells) => cells.join(" ").trimEnd();
2064
+ const lines = [chalk14.bold(join5(padCells(HEADERS)))];
1840
2065
  for (const row of rows) {
1841
2066
  const padded = padCells(row);
1842
2067
  const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
1843
2068
  padded[2] = colorFn(row[2]) + padded[2].slice(row[2].length);
1844
- lines.push(join4(padded));
2069
+ lines.push(join5(padded));
1845
2070
  }
1846
2071
  return lines.join("\n") + "\n";
1847
2072
  }
@@ -1895,7 +2120,7 @@ async function buildsLogs(id, opts, deps) {
1895
2120
  console.log(JSON.stringify(logs));
1896
2121
  } else {
1897
2122
  if (logs.content) process.stdout.write(logs.content);
1898
- console.error(chalk14.dim(`status: ${logs.status}`));
2123
+ console.error(chalk15.dim(`status: ${logs.status}`));
1899
2124
  }
1900
2125
  process.exitCode = 0;
1901
2126
  return;
@@ -1992,7 +2217,7 @@ async function buildsDownload(id, opts, deps) {
1992
2217
  if (opts.json) {
1993
2218
  console.log(JSON.stringify(result));
1994
2219
  } else {
1995
- console.log(chalk14.green(`\u2713 saved ${result.path}`));
2220
+ console.log(chalk15.green(`\u2713 saved ${result.path}`));
1996
2221
  }
1997
2222
  process.exitCode = 0;
1998
2223
  } catch (err) {
@@ -2009,7 +2234,7 @@ async function buildsCancel(id, opts, deps) {
2009
2234
  if (opts.json) {
2010
2235
  console.log(JSON.stringify(build));
2011
2236
  } else {
2012
- console.log(`${chalk14.cyan(build.id)} ${build.status}`);
2237
+ console.log(`${chalk15.cyan(build.id)} ${build.status}`);
2013
2238
  }
2014
2239
  process.exitCode = 0;
2015
2240
  } catch (err) {
@@ -2026,7 +2251,7 @@ async function buildsDeleteArtifact(id, opts, deps) {
2026
2251
  if (opts.json) {
2027
2252
  console.log(JSON.stringify(result));
2028
2253
  } else {
2029
- console.log(chalk14.green(`\u2713 deleted artifact for ${id}`));
2254
+ console.log(chalk15.green(`\u2713 deleted artifact for ${id}`));
2030
2255
  }
2031
2256
  process.exitCode = 0;
2032
2257
  } catch (err) {
@@ -2037,11 +2262,11 @@ async function buildsDeleteArtifact(id, opts, deps) {
2037
2262
  function printError(opts, err) {
2038
2263
  const message = err instanceof Error ? err.message : String(err);
2039
2264
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2040
- console.error(chalk14.red(`error: ${message}`));
2265
+ console.error(chalk15.red(`error: ${message}`));
2041
2266
  }
2042
2267
 
2043
2268
  // src/commands/submit.ts
2044
- import chalk15 from "chalk";
2269
+ import chalk16 from "chalk";
2045
2270
  function defaultDeps2(client) {
2046
2271
  return {
2047
2272
  createSubmission: client.createSubmission.bind(client),
@@ -2065,7 +2290,7 @@ function submitConfigFor(cwd, platform) {
2065
2290
  cfg = loadDevConfig(cwd);
2066
2291
  } catch (err) {
2067
2292
  console.error(
2068
- chalk15.yellow(
2293
+ chalk16.yellow(
2069
2294
  `warning: vibeview.json could not be read \u2014 ignoring submit defaults (${err instanceof Error ? err.message : String(err)})`
2070
2295
  )
2071
2296
  );
@@ -2213,12 +2438,12 @@ async function submitCommand(opts, deps) {
2213
2438
  return;
2214
2439
  }
2215
2440
  if (!opts.json) {
2216
- console.error(` ${chalk15.cyan(sub.id)} ${chalk15.dim(`[${sub.platform}]`)}`);
2441
+ console.error(` ${chalk16.cyan(sub.id)} ${chalk16.dim(`[${sub.platform}]`)}`);
2217
2442
  }
2218
2443
  const final = await followSubmission(sub, opts, d);
2219
2444
  if (!opts.json && final.status === "succeeded" && (final.release_status ?? releaseStatus) === "draft") {
2220
2445
  console.error(
2221
- chalk15.dim("release status: draft \u2014 activate it from the Play Console when ready")
2446
+ chalk16.dim("release status: draft \u2014 activate it from the Play Console when ready")
2222
2447
  );
2223
2448
  }
2224
2449
  process.exitCode = final.status === "succeeded" ? 0 : 1;
@@ -2249,11 +2474,11 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
2249
2474
  });
2250
2475
  emit(opts.json, { event: "preflight", ...result });
2251
2476
  if (!opts.json) {
2252
- const mark = result.ready ? chalk15.green("\u2713") : chalk15.red("\u2717");
2477
+ const mark = result.ready ? chalk16.green("\u2713") : chalk16.red("\u2717");
2253
2478
  console.error(`${mark} ${result.message}`);
2254
2479
  if (result.ready) {
2255
2480
  console.error(
2256
- chalk15.dim(
2481
+ chalk16.dim(
2257
2482
  ` next: vibeview build --cloud --production --platform ${platform}, then vibeview submit --platform ${platform} --latest`
2258
2483
  )
2259
2484
  );
@@ -2287,7 +2512,7 @@ async function linkedIdentifier(platform, opts, deps) {
2287
2512
  }
2288
2513
  if (!app.package_name) return void 0;
2289
2514
  if (!opts.json) {
2290
- console.error(chalk15.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2515
+ console.error(chalk16.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2291
2516
  }
2292
2517
  return app.package_name;
2293
2518
  }
@@ -2328,14 +2553,14 @@ async function followSubmission(created, opts, deps) {
2328
2553
  if (opts.json) {
2329
2554
  emit(opts.json, { event: "status", id: created.id, status: "queued" });
2330
2555
  } else {
2331
- console.error(chalk15.dim("\u23F3 queued \u2014 waiting for a submission slot"));
2556
+ console.error(chalk16.dim("\u23F3 queued \u2014 waiting for a submission slot"));
2332
2557
  }
2333
2558
  } else if (log.status === "running" && !printedRunning) {
2334
2559
  printedRunning = true;
2335
2560
  if (opts.json) {
2336
2561
  emit(opts.json, { event: "status", id: created.id, status: "running" });
2337
2562
  } else {
2338
- console.error(`${chalk15.cyan("\u25B6")} submitting`);
2563
+ console.error(`${chalk16.cyan("\u25B6")} submitting`);
2339
2564
  }
2340
2565
  }
2341
2566
  if (log.content) {
@@ -2377,7 +2602,7 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2377
2602
  });
2378
2603
  if (!opts.json) {
2379
2604
  const ok = final.status === "succeeded";
2380
- const mark = ok ? chalk15.green("\u2713") : chalk15.red("\u2717");
2605
+ const mark = ok ? chalk16.green("\u2713") : chalk16.red("\u2717");
2381
2606
  const duration = formatDuration(final.started_at, final.finished_at);
2382
2607
  let line = `${mark} ${final.status}`;
2383
2608
  if (duration) line += ` (${duration})`;
@@ -2389,11 +2614,11 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2389
2614
  function printError2(opts, err) {
2390
2615
  const message = err instanceof Error ? err.message : String(err);
2391
2616
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2392
- console.error(chalk15.red(`error: ${message}`));
2617
+ console.error(chalk16.red(`error: ${message}`));
2393
2618
  }
2394
2619
 
2395
2620
  // src/commands/versions.ts
2396
- import chalk16 from "chalk";
2621
+ import chalk17 from "chalk";
2397
2622
  function defaultDeps3(client) {
2398
2623
  return {
2399
2624
  listAppVersions: client.listAppVersions.bind(client),
@@ -2443,7 +2668,7 @@ async function versionsSet(buildNumber, opts, deps) {
2443
2668
  console.log(JSON.stringify(version));
2444
2669
  } else {
2445
2670
  console.log(
2446
- chalk16.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk16.dim(
2671
+ chalk17.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk17.dim(
2447
2672
  " \u2014 the next autoIncrement production build gets " + (version.build_number + 1)
2448
2673
  )
2449
2674
  );
@@ -2462,19 +2687,19 @@ function formatVersionsHuman(versions2) {
2462
2687
  const rows = versions2.map((v) => [v.platform, String(v.build_number), v.updated_at ?? ""]);
2463
2688
  const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2464
2689
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2465
- const join4 = (cells) => cells.join(" ").trimEnd();
2466
- const lines = [chalk16.bold(join4(padCells(HEADERS2)))];
2467
- for (const row of rows) lines.push(join4(padCells(row)));
2690
+ const join5 = (cells) => cells.join(" ").trimEnd();
2691
+ const lines = [chalk17.bold(join5(padCells(HEADERS2)))];
2692
+ for (const row of rows) lines.push(join5(padCells(row)));
2468
2693
  return lines.join("\n") + "\n";
2469
2694
  }
2470
2695
  function printError3(opts, err) {
2471
2696
  const message = err instanceof Error ? err.message : String(err);
2472
2697
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2473
- console.error(chalk16.red(`error: ${message}`));
2698
+ console.error(chalk17.red(`error: ${message}`));
2474
2699
  }
2475
2700
 
2476
2701
  // src/commands/whoami.ts
2477
- import chalk17 from "chalk";
2702
+ import chalk18 from "chalk";
2478
2703
  function defaultDeps4(opts) {
2479
2704
  const { token, baseUrl } = resolveAuth(opts);
2480
2705
  const client = new ApiClient(baseUrl, token);
@@ -2505,33 +2730,33 @@ async function whoAmI(opts, deps) {
2505
2730
  process.exitCode = 0;
2506
2731
  return;
2507
2732
  }
2508
- console.log(`${chalk17.bold("Logged in as")} ${me.email}`);
2733
+ console.log(`${chalk18.bold("Logged in as")} ${me.email}`);
2509
2734
  if (me.memberships.length === 1) {
2510
- console.log(`${chalk17.bold("Organization")} ${me.memberships[0].organization_name}`);
2735
+ console.log(`${chalk18.bold("Organization")} ${me.memberships[0].organization_name}`);
2511
2736
  } else if (me.memberships.length > 1) {
2512
- console.log(chalk17.bold("Organizations"));
2737
+ console.log(chalk18.bold("Organizations"));
2513
2738
  for (const m of me.memberships) {
2514
2739
  console.log(` ${m.organization_name} (${m.role})`);
2515
2740
  }
2516
2741
  }
2517
- console.log(`${chalk17.bold("Member role")} ${me.role ?? "\u2014"}`);
2518
- console.log(`${chalk17.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
2519
- console.log(`${chalk17.bold("Effective role")} ${effective ?? "\u2014"}`);
2742
+ console.log(`${chalk18.bold("Member role")} ${me.role ?? "\u2014"}`);
2743
+ console.log(`${chalk18.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
2744
+ console.log(`${chalk18.bold("Effective role")} ${effective ?? "\u2014"}`);
2520
2745
  if (me.token_access) {
2521
2746
  console.log(
2522
- `${chalk17.bold("Access")} ${me.token_access === "ci" ? "CI only (upload builds, run tests, read results)" : "Full access"}`
2747
+ `${chalk18.bold("Access")} ${me.token_access === "ci" ? "CI only (upload builds, run tests, read results)" : "Full access"}`
2523
2748
  );
2524
- console.log(`${chalk17.bold("Expires")} ${describeExpiry(me.token_expires_at)}`);
2749
+ console.log(`${chalk18.bold("Expires")} ${describeExpiry(me.token_expires_at)}`);
2525
2750
  }
2526
2751
  if (me.is_superadmin) {
2527
- console.log(chalk17.dim("Superadmin (platform-wide; does not raise the org role above)"));
2752
+ console.log(chalk18.dim("Superadmin (platform-wide; does not raise the org role above)"));
2528
2753
  }
2529
2754
  if (me.role === "admin" && me.token_role && me.token_role !== "admin" && me.token_access !== "ci") {
2530
2755
  console.log(
2531
- chalk17.yellow(
2756
+ chalk18.yellow(
2532
2757
  `
2533
- Your account is admin, but this CLI token is ${me.token_role}. The effective role is the lower of the two, so admin-only commands (such as ${chalk17.bold("vibeview env set")}) will fail with a permissions error.
2534
- Create an admin API token in the dashboard and use ${chalk17.bold("--token")} or ${chalk17.bold("VIBEVIEW_API_TOKEN")}.`
2758
+ Your account is admin, but this CLI token is ${me.token_role}. The effective role is the lower of the two, so admin-only commands (such as ${chalk18.bold("vibeview env set")}) will fail with a permissions error.
2759
+ Create an admin API token in the dashboard and use ${chalk18.bold("--token")} or ${chalk18.bold("VIBEVIEW_API_TOKEN")}.`
2535
2760
  )
2536
2761
  );
2537
2762
  }
@@ -2541,15 +2766,15 @@ Create an admin API token in the dashboard and use ${chalk17.bold("--token")} or
2541
2766
  if (opts.json) {
2542
2767
  console.log(JSON.stringify({ event: "error", message }));
2543
2768
  }
2544
- console.error(chalk17.red(`error: ${message}`));
2769
+ console.error(chalk18.red(`error: ${message}`));
2545
2770
  process.exitCode = errorExitCode(err);
2546
2771
  }
2547
2772
  }
2548
2773
 
2549
2774
  // src/commands/env.ts
2550
2775
  import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
2551
- import { basename } from "path";
2552
- import chalk18 from "chalk";
2776
+ import { basename as basename2 } from "path";
2777
+ import chalk19 from "chalk";
2553
2778
  import { password } from "@inquirer/prompts";
2554
2779
 
2555
2780
  // src/lib/gitignore.ts
@@ -2605,7 +2830,7 @@ function defaultDeps5(client) {
2605
2830
  isIgnored: (path) => isGitIgnored(path),
2606
2831
  writeFile: writeSecureFile,
2607
2832
  readFile: (path) => readFileAsync(path),
2608
- warn: (message) => console.error(chalk18.yellow(`warning: ${message}`))
2833
+ warn: (message) => console.error(chalk19.yellow(`warning: ${message}`))
2609
2834
  };
2610
2835
  }
2611
2836
  async function envList(opts, deps) {
@@ -2636,9 +2861,9 @@ function formatEnvVarsHuman(variables) {
2636
2861
  ]);
2637
2862
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2638
2863
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2639
- const join4 = (cells) => cells.join(" ").trimEnd();
2640
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
2641
- for (const row of rows) lines.push(join4(padCells(row)));
2864
+ const join5 = (cells) => cells.join(" ").trimEnd();
2865
+ const lines = [chalk19.bold(join5(padCells(HEADERS3)))];
2866
+ for (const row of rows) lines.push(join5(padCells(row)));
2642
2867
  return lines.join("\n") + "\n";
2643
2868
  }
2644
2869
  async function envSet(name, value, opts, deps) {
@@ -2655,7 +2880,7 @@ async function envSet(name, value, opts, deps) {
2655
2880
  const visibility = resolveVisibility(opts);
2656
2881
  if (opts.file) {
2657
2882
  const content = await d.readFile(opts.file);
2658
- const filename = basename(opts.file);
2883
+ const filename = basename2(opts.file);
2659
2884
  const result2 = await d.setFileVar({ name, content, filename, visibility, app: opts.app });
2660
2885
  printSetResult(opts, result2);
2661
2886
  process.exitCode = 0;
@@ -2687,7 +2912,7 @@ function printSetResult(opts, result) {
2687
2912
  if (opts.json) {
2688
2913
  console.log(JSON.stringify(result));
2689
2914
  } else {
2690
- console.log(chalk18.green(`\u2713 set ${result.name} (${result.visibility})`));
2915
+ console.log(chalk19.green(`\u2713 set ${result.name} (${result.visibility})`));
2691
2916
  }
2692
2917
  }
2693
2918
  async function envRm(name, opts, deps) {
@@ -2697,7 +2922,7 @@ async function envRm(name, opts, deps) {
2697
2922
  if (opts.json) {
2698
2923
  console.log(JSON.stringify({ event: "deleted", name }));
2699
2924
  } else {
2700
- console.log(chalk18.green(`\u2713 removed ${name}`));
2925
+ console.log(chalk19.green(`\u2713 removed ${name}`));
2701
2926
  }
2702
2927
  process.exitCode = 0;
2703
2928
  } catch (err) {
@@ -2729,7 +2954,7 @@ async function envPull(opts, deps) {
2729
2954
  if (opts.json) {
2730
2955
  console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
2731
2956
  } else {
2732
- console.log(chalk18.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
2957
+ console.log(chalk19.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
2733
2958
  }
2734
2959
  process.exitCode = 0;
2735
2960
  } catch (err) {
@@ -2768,7 +2993,7 @@ function escapeEnvValue(value) {
2768
2993
  function printError4(opts, err) {
2769
2994
  const message = err instanceof Error ? err.message : String(err);
2770
2995
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2771
- console.error(chalk18.red(`error: ${message}`));
2996
+ console.error(chalk19.red(`error: ${message}`));
2772
2997
  }
2773
2998
 
2774
2999
  // src/commands/credentials.ts
@@ -2779,8 +3004,8 @@ import {
2779
3004
  writeFile as writeFileAsync
2780
3005
  } from "fs/promises";
2781
3006
  import { mkdirSync } from "fs";
2782
- import { basename as basename2, extname, join } from "path";
2783
- import chalk19 from "chalk";
3007
+ import { basename as basename3, extname as extname2, join as join2 } from "path";
3008
+ import chalk20 from "chalk";
2784
3009
  import { input, password as password2 } from "@inquirer/prompts";
2785
3010
  function clientFor2(opts) {
2786
3011
  const { token, baseUrl } = resolveAuth(opts);
@@ -2853,7 +3078,7 @@ function defaultDeps6(client) {
2853
3078
  mkdir: (dir) => mkdirSync(dir, { recursive: true }),
2854
3079
  writeFile: (path, data, mode) => writeFileAsync(path, data, mode !== void 0 ? { mode } : void 0),
2855
3080
  chmod: (path, mode) => chmodAsync(path, mode),
2856
- warn: (message) => console.error(chalk19.yellow(`warning: ${message}`))
3081
+ warn: (message) => console.error(chalk20.yellow(`warning: ${message}`))
2857
3082
  };
2858
3083
  }
2859
3084
  var ANDROID_KIND = "android_keystore";
@@ -2883,10 +3108,10 @@ async function credentialsAdd(opts, deps) {
2883
3108
  "a password is required: run interactively to be prompted (passwords are never accepted as flags)."
2884
3109
  );
2885
3110
  }
2886
- const name = opts.name ?? basename2(opts.file, extname(opts.file));
3111
+ const name = opts.name ?? basename3(opts.file, extname2(opts.file));
2887
3112
  try {
2888
3113
  const file = await d.readFile(opts.file);
2889
- const filename = basename2(opts.file);
3114
+ const filename = basename3(opts.file);
2890
3115
  let body;
2891
3116
  if (platform === "android") {
2892
3117
  const storePassword = await d.prompt({ message: "Keystore (store) password", hidden: true });
@@ -2915,14 +3140,14 @@ async function credentialsAdd(opts, deps) {
2915
3140
  file,
2916
3141
  filename,
2917
3142
  profile,
2918
- profileFilename: basename2(opts.profile)
3143
+ profileFilename: basename3(opts.profile)
2919
3144
  };
2920
3145
  }
2921
3146
  const result = await d.addCredential(body);
2922
3147
  if (opts.json) {
2923
3148
  console.log(JSON.stringify(result));
2924
3149
  } else {
2925
- console.log(chalk19.green(`\u2713 added ${result.name} (${result.platform})`));
3150
+ console.log(chalk20.green(`\u2713 added ${result.name} (${result.platform})`));
2926
3151
  }
2927
3152
  process.exitCode = 0;
2928
3153
  } catch (err) {
@@ -2948,7 +3173,7 @@ async function credentialsGenerate(opts, deps) {
2948
3173
  const outDir = opts.out ?? process.cwd();
2949
3174
  try {
2950
3175
  d.mkdir(outDir);
2951
- const expectedPath = join(outDir, `${opts.name}.keystore`);
3176
+ const expectedPath = join2(outDir, `${opts.name}.keystore`);
2952
3177
  if (await d.fileExists(expectedPath)) {
2953
3178
  throw new Error(
2954
3179
  `${expectedPath} already exists \u2014 refusing to overwrite it. Move it away or pass a different --out/--name.`
@@ -2963,7 +3188,7 @@ async function credentialsGenerate(opts, deps) {
2963
3188
  // charge, so a stale CLI can never accidentally opt out of it.
2964
3189
  ...opts.force ? { overwrite: true } : {}
2965
3190
  });
2966
- const savedTo = join(outDir, basename2(result.filename));
3191
+ const savedTo = join2(outDir, basename3(result.filename));
2967
3192
  if (opts.json) {
2968
3193
  console.log(JSON.stringify({ ...result, saved_to: savedTo }));
2969
3194
  }
@@ -2980,20 +3205,20 @@ async function credentialsGenerate(opts, deps) {
2980
3205
  }
2981
3206
  if (opts.json) {
2982
3207
  } else {
2983
- console.log(chalk19.green(`\u2713 generated ${result.name} (android)`));
3208
+ console.log(chalk20.green(`\u2713 generated ${result.name} (android)`));
2984
3209
  console.log("");
2985
- console.log(` Keystore saved to: ${chalk19.bold(savedTo)}`);
2986
- console.log(` Key alias: ${chalk19.bold(result.key_alias)}`);
2987
- console.log(` Store password: ${chalk19.bold(result.store_password)}`);
2988
- console.log(` Key password: ${chalk19.bold(result.key_password)}`);
3210
+ console.log(` Keystore saved to: ${chalk20.bold(savedTo)}`);
3211
+ console.log(` Key alias: ${chalk20.bold(result.key_alias)}`);
3212
+ console.log(` Store password: ${chalk20.bold(result.store_password)}`);
3213
+ console.log(` Key password: ${chalk20.bold(result.key_password)}`);
2989
3214
  console.log("");
2990
3215
  console.log(
2991
- chalk19.yellow(
3216
+ chalk20.yellow(
2992
3217
  " These are shown ONCE \u2014 VibeView cannot recover the keystore file or its passwords. Back them up somewhere safe."
2993
3218
  )
2994
3219
  );
2995
3220
  console.log(
2996
- chalk19.yellow(
3221
+ chalk20.yellow(
2997
3222
  " The credential is already uploaded and active for android and androidtv production builds."
2998
3223
  )
2999
3224
  );
@@ -3019,19 +3244,19 @@ async function generateAppleCredential(platform, opts, d) {
3019
3244
  if (opts.json) {
3020
3245
  console.log(JSON.stringify(result));
3021
3246
  } else {
3022
- console.log(chalk19.green(`\u2713 generated ${result.name} (${result.platform})`));
3247
+ console.log(chalk20.green(`\u2713 generated ${result.name} (${result.platform})`));
3023
3248
  console.log("");
3024
- console.log(` Scope: ${chalk19.bold(result.app_public_id ?? "org-wide")}`);
3249
+ console.log(` Scope: ${chalk20.bold(result.app_public_id ?? "org-wide")}`);
3025
3250
  const expires = formatExpires(result);
3026
3251
  if (expires) {
3027
- console.log(` Expires: ${chalk19.bold(expires)}`);
3252
+ console.log(` Expires: ${chalk20.bold(expires)}`);
3028
3253
  }
3029
3254
  console.log("");
3030
3255
  console.log(
3031
3256
  ` The certificate and provisioning profile are stored with VibeView and already active for ${result.platform} production builds.`
3032
3257
  );
3033
3258
  console.log(
3034
- ` Run ${chalk19.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
3259
+ ` Run ${chalk20.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
3035
3260
  );
3036
3261
  }
3037
3262
  process.exitCode = 0;
@@ -3072,8 +3297,8 @@ async function credentialsDownload(name, opts, deps) {
3072
3297
  }
3073
3298
  throw err;
3074
3299
  }
3075
- const savedTo = join(outDir, basename2(material.filename));
3076
- const profileSavedTo = material.profile_base64 != null ? join(outDir, `${basename2(material.filename, extname(material.filename))}.mobileprovision`) : null;
3300
+ const savedTo = join2(outDir, basename3(material.filename));
3301
+ const profileSavedTo = material.profile_base64 != null ? join2(outDir, `${basename3(material.filename, extname2(material.filename))}.mobileprovision`) : null;
3077
3302
  if (opts.json) {
3078
3303
  console.log(
3079
3304
  JSON.stringify({
@@ -3097,17 +3322,17 @@ async function credentialsDownload(name, opts, deps) {
3097
3322
  await d.chmod(path, 384);
3098
3323
  }
3099
3324
  if (!opts.json) {
3100
- console.log(chalk19.green(`\u2713 downloaded ${material.name} (${material.platform})`));
3325
+ console.log(chalk20.green(`\u2713 downloaded ${material.name} (${material.platform})`));
3101
3326
  console.log("");
3102
- console.log(` Saved to: ${chalk19.bold(savedTo)}`);
3327
+ console.log(` Saved to: ${chalk20.bold(savedTo)}`);
3103
3328
  if (profileSavedTo !== null) {
3104
- console.log(` Profile: ${chalk19.bold(profileSavedTo)}`);
3329
+ console.log(` Profile: ${chalk20.bold(profileSavedTo)}`);
3105
3330
  }
3106
3331
  for (const [key, value] of Object.entries(material.passwords)) {
3107
- console.log(` ${key}: ${chalk19.bold(value)}`);
3332
+ console.log(` ${key}: ${chalk20.bold(value)}`);
3108
3333
  }
3109
3334
  console.log("");
3110
- console.log(chalk19.yellow(" This download is recorded in your organization's audit log."));
3335
+ console.log(chalk20.yellow(" This download is recorded in your organization's audit log."));
3111
3336
  }
3112
3337
  process.exitCode = 0;
3113
3338
  } catch (err) {
@@ -3133,18 +3358,18 @@ async function ascKeyAdd(opts, deps) {
3133
3358
  keyId: opts.keyId,
3134
3359
  issuerId: opts.issuerId,
3135
3360
  file,
3136
- filename: basename2(opts.file)
3361
+ filename: basename3(opts.file)
3137
3362
  });
3138
3363
  if (opts.json) {
3139
3364
  console.log(JSON.stringify(result));
3140
3365
  } else {
3141
- console.log(chalk19.green(`\u2713 added App Store Connect key ${result.name}`));
3366
+ console.log(chalk20.green(`\u2713 added App Store Connect key ${result.name}`));
3142
3367
  console.log("");
3143
- console.log(` Key ID: ${chalk19.bold(result.key_id)}`);
3144
- console.log(` Issuer ID: ${chalk19.bold(result.issuer_id)}`);
3368
+ console.log(` Key ID: ${chalk20.bold(result.key_id)}`);
3369
+ console.log(` Issuer ID: ${chalk20.bold(result.issuer_id)}`);
3145
3370
  console.log("");
3146
3371
  console.log(
3147
- chalk19.yellow(
3372
+ chalk20.yellow(
3148
3373
  " The .p8 is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
3149
3374
  )
3150
3375
  );
@@ -3178,9 +3403,9 @@ function formatAscKeysHuman(keys) {
3178
3403
  const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, formatUtcTimestamp(k.created_at)]);
3179
3404
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3180
3405
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3181
- const join4 = (cells) => cells.join(" ").trimEnd();
3182
- const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3183
- for (const row of rows) lines.push(join4(padCells(row)));
3406
+ const join5 = (cells) => cells.join(" ").trimEnd();
3407
+ const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
3408
+ for (const row of rows) lines.push(join5(padCells(row)));
3184
3409
  return lines.join("\n") + "\n";
3185
3410
  }
3186
3411
  async function ascKeyRm(name, opts, deps) {
@@ -3190,7 +3415,7 @@ async function ascKeyRm(name, opts, deps) {
3190
3415
  if (opts.json) {
3191
3416
  console.log(JSON.stringify({ event: "deleted", name }));
3192
3417
  } else {
3193
- console.log(chalk19.green(`\u2713 removed App Store Connect key ${name}`));
3418
+ console.log(chalk20.green(`\u2713 removed App Store Connect key ${name}`));
3194
3419
  }
3195
3420
  process.exitCode = 0;
3196
3421
  } catch (err) {
@@ -3210,17 +3435,17 @@ async function playKeyAdd(opts, deps) {
3210
3435
  const result = await d.addPlayKey({
3211
3436
  ...opts.name !== void 0 ? { name: opts.name } : {},
3212
3437
  file,
3213
- filename: basename2(opts.file)
3438
+ filename: basename3(opts.file)
3214
3439
  });
3215
3440
  if (opts.json) {
3216
3441
  console.log(JSON.stringify(result));
3217
3442
  } else {
3218
- console.log(chalk19.green(`\u2713 added Google Play key ${result.name}`));
3443
+ console.log(chalk20.green(`\u2713 added Google Play key ${result.name}`));
3219
3444
  console.log("");
3220
- console.log(` Service account: ${chalk19.bold(result.client_email)}`);
3445
+ console.log(` Service account: ${chalk20.bold(result.client_email)}`);
3221
3446
  console.log("");
3222
3447
  console.log(
3223
- chalk19.yellow(
3448
+ chalk20.yellow(
3224
3449
  " The JSON key is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
3225
3450
  )
3226
3451
  );
@@ -3254,9 +3479,9 @@ function formatPlayKeysHuman(keys) {
3254
3479
  const rows = keys.map((k) => [k.name, k.client_email, formatUtcTimestamp(k.created_at)]);
3255
3480
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3256
3481
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3257
- const join4 = (cells) => cells.join(" ").trimEnd();
3258
- const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3259
- for (const row of rows) lines.push(join4(padCells(row)));
3482
+ const join5 = (cells) => cells.join(" ").trimEnd();
3483
+ const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
3484
+ for (const row of rows) lines.push(join5(padCells(row)));
3260
3485
  return lines.join("\n") + "\n";
3261
3486
  }
3262
3487
  async function playKeyRm(name, opts, deps) {
@@ -3266,7 +3491,7 @@ async function playKeyRm(name, opts, deps) {
3266
3491
  if (opts.json) {
3267
3492
  console.log(JSON.stringify({ event: "deleted", name }));
3268
3493
  } else {
3269
- console.log(chalk19.green(`\u2713 removed Google Play key ${name}`));
3494
+ console.log(chalk20.green(`\u2713 removed Google Play key ${name}`));
3270
3495
  }
3271
3496
  process.exitCode = 0;
3272
3497
  } catch (err) {
@@ -3334,9 +3559,9 @@ function formatCredentialsHuman(credentials2) {
3334
3559
  ]);
3335
3560
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3336
3561
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3337
- const join4 = (cells) => cells.join(" ").trimEnd();
3338
- const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3339
- for (const row of rows) lines.push(join4(padCells(row)));
3562
+ const join5 = (cells) => cells.join(" ").trimEnd();
3563
+ const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
3564
+ for (const row of rows) lines.push(join5(padCells(row)));
3340
3565
  return lines.join("\n") + "\n";
3341
3566
  }
3342
3567
  async function credentialsRm(name, opts, deps) {
@@ -3346,7 +3571,7 @@ async function credentialsRm(name, opts, deps) {
3346
3571
  if (opts.json) {
3347
3572
  console.log(JSON.stringify({ event: "deleted", name }));
3348
3573
  } else {
3349
- console.log(chalk19.green(`\u2713 removed ${name}`));
3574
+ console.log(chalk20.green(`\u2713 removed ${name}`));
3350
3575
  }
3351
3576
  process.exitCode = 0;
3352
3577
  } catch (err) {
@@ -3358,16 +3583,16 @@ function printKeystoreWriteFailure(opts, err, result) {
3358
3583
  const message = err instanceof Error ? err.message : String(err);
3359
3584
  if (opts.json) {
3360
3585
  console.log(JSON.stringify({ event: "error", message }));
3361
- console.error(chalk19.red(`error: ${message}`));
3586
+ console.error(chalk20.red(`error: ${message}`));
3362
3587
  return;
3363
3588
  }
3364
- console.error(chalk19.red(`error: ${message}`));
3589
+ console.error(chalk20.red(`error: ${message}`));
3365
3590
  console.error(
3366
- chalk19.red(
3591
+ chalk20.red(
3367
3592
  `The keystore could NOT be saved locally, but the credential "${result.name}" WAS created server-side and its key material can never be re-fetched.`
3368
3593
  )
3369
3594
  );
3370
- console.error(chalk19.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
3595
+ console.error(chalk20.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
3371
3596
  console.error("");
3372
3597
  console.error(` Key alias: ${result.key_alias}`);
3373
3598
  console.error(` Store password: ${result.store_password}`);
@@ -3381,11 +3606,11 @@ function printKeystoreWriteFailure(opts, err, result) {
3381
3606
  function printError5(opts, err) {
3382
3607
  const message = err instanceof Error ? err.message : String(err);
3383
3608
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3384
- console.error(chalk19.red(`error: ${message}`));
3609
+ console.error(chalk20.red(`error: ${message}`));
3385
3610
  }
3386
3611
 
3387
3612
  // src/commands/webhooks.ts
3388
- import chalk20 from "chalk";
3613
+ import chalk21 from "chalk";
3389
3614
  function clientFor3(opts) {
3390
3615
  const { token, baseUrl } = resolveAuth(opts);
3391
3616
  return new ApiClient(baseUrl, token);
@@ -3416,14 +3641,14 @@ async function webhooksAdd(opts, deps) {
3416
3641
  if (opts.json) {
3417
3642
  console.log(JSON.stringify(result));
3418
3643
  } else {
3419
- console.log(chalk20.green(`\u2713 added ${result.id} ${result.url}`));
3644
+ console.log(chalk21.green(`\u2713 added ${result.id} ${result.url}`));
3420
3645
  console.log("");
3421
- console.log(` Signing secret: ${chalk20.bold(result.secret)}`);
3646
+ console.log(` Signing secret: ${chalk21.bold(result.secret)}`);
3422
3647
  if (opts.secret !== void 0) {
3423
- console.log(chalk20.yellow(" (This is the secret you provided.)"));
3648
+ console.log(chalk21.yellow(" (This is the secret you provided.)"));
3424
3649
  }
3425
3650
  console.log(
3426
- chalk20.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
3651
+ chalk21.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
3427
3652
  );
3428
3653
  }
3429
3654
  process.exitCode = 0;
@@ -3453,9 +3678,9 @@ function formatWebhooksHuman(webhooks2) {
3453
3678
  ]);
3454
3679
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3455
3680
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3456
- const join4 = (cells) => cells.join(" ").trimEnd();
3457
- const lines = [chalk20.bold(join4(padCells(HEADERS3)))];
3458
- for (const row of rows) lines.push(join4(padCells(row)));
3681
+ const join5 = (cells) => cells.join(" ").trimEnd();
3682
+ const lines = [chalk21.bold(join5(padCells(HEADERS3)))];
3683
+ for (const row of rows) lines.push(join5(padCells(row)));
3459
3684
  return lines.join("\n") + "\n";
3460
3685
  }
3461
3686
  async function webhooksList(opts, deps) {
@@ -3480,7 +3705,7 @@ async function webhooksRm(id, opts, deps) {
3480
3705
  if (opts.json) {
3481
3706
  console.log(JSON.stringify({ event: "deleted", id }));
3482
3707
  } else {
3483
- console.log(chalk20.green(`\u2713 removed ${id}`));
3708
+ console.log(chalk21.green(`\u2713 removed ${id}`));
3484
3709
  }
3485
3710
  process.exitCode = 0;
3486
3711
  } catch (err) {
@@ -3513,13 +3738,13 @@ async function webhooksTest(id, opts, deps) {
3513
3738
  if (opts.json) console.log(JSON.stringify(ld));
3514
3739
  if (ld.status === "succeeded") {
3515
3740
  if (!opts.json) {
3516
- console.log(chalk20.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3741
+ console.log(chalk21.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3517
3742
  }
3518
3743
  process.exitCode = 0;
3519
3744
  } else {
3520
3745
  if (!opts.json) {
3521
3746
  const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
3522
- console.error(chalk20.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3747
+ console.error(chalk21.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3523
3748
  }
3524
3749
  process.exitCode = 1;
3525
3750
  }
@@ -3528,7 +3753,7 @@ async function webhooksTest(id, opts, deps) {
3528
3753
  }
3529
3754
  if (opts.json) console.log(JSON.stringify({ status: "pending" }));
3530
3755
  console.error(
3531
- chalk20.yellow(
3756
+ chalk21.yellow(
3532
3757
  `Delivery still pending after ${TEST_POLL_BUDGET_MS / 1e3}s \u2014 check \`vibeview webhooks list\` later for the result.`
3533
3758
  )
3534
3759
  );
@@ -3541,7 +3766,7 @@ async function webhooksTest(id, opts, deps) {
3541
3766
  function printError6(opts, err) {
3542
3767
  const message = err instanceof Error ? err.message : String(err);
3543
3768
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3544
- console.error(chalk20.red(`error: ${message}`));
3769
+ console.error(chalk21.red(`error: ${message}`));
3545
3770
  }
3546
3771
 
3547
3772
  // src/commands/list-devices.ts
@@ -3582,7 +3807,7 @@ async function listDevicesCommand(options) {
3582
3807
  }
3583
3808
 
3584
3809
  // src/lib/metadata.ts
3585
- import { readFileSync as readFileSync2 } from "fs";
3810
+ import { readFileSync as readFileSync3 } from "fs";
3586
3811
  var MAX_METADATA_BYTES = 16384;
3587
3812
  var MAX_METADATA_DEPTH = 4;
3588
3813
  function measureDepth(value, current = 1) {
@@ -3621,7 +3846,7 @@ function parseMetadata(raw) {
3621
3846
  function readMetadataFile(path) {
3622
3847
  let raw;
3623
3848
  try {
3624
- raw = readFileSync2(path, "utf8");
3849
+ raw = readFileSync3(path, "utf8");
3625
3850
  } catch (e) {
3626
3851
  throw new Error(`failed to read --metadata-file ${path}: ${e.message}`);
3627
3852
  }
@@ -3633,27 +3858,27 @@ function readMetadataFile(path) {
3633
3858
  }
3634
3859
 
3635
3860
  // src/commands/agent-setup.ts
3636
- import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
3861
+ import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
3637
3862
  import { homedir } from "os";
3638
- import { dirname, join as join2 } from "path";
3863
+ import { dirname, join as join3 } from "path";
3639
3864
  import { fileURLToPath } from "url";
3640
3865
  import { spawnSync } from "child_process";
3641
3866
  import { confirm } from "@inquirer/prompts";
3642
- import chalk21 from "chalk";
3867
+ import chalk22 from "chalk";
3643
3868
  function resolveShippedSkillPath(fromDir) {
3644
- for (const rel of ["..", join2("..", "..")]) {
3645
- const candidate = join2(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
3646
- if (existsSync2(candidate)) return candidate;
3869
+ for (const rel of ["..", join3("..", "..")]) {
3870
+ const candidate = join3(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
3871
+ if (existsSync3(candidate)) return candidate;
3647
3872
  }
3648
3873
  return null;
3649
3874
  }
3650
3875
  function skillInstallDir(project, cwd, home) {
3651
3876
  const base = project ? cwd : home;
3652
- return join2(base, ".claude", "skills", "vibeview-agent");
3877
+ return join3(base, ".claude", "skills", "vibeview-agent");
3653
3878
  }
3654
3879
  function installSkill(shippedPath, targetDir) {
3655
- const target = join2(targetDir, "SKILL.md");
3656
- const existed = existsSync2(target);
3880
+ const target = join3(targetDir, "SKILL.md");
3881
+ const existed = existsSync3(target);
3657
3882
  mkdirSync2(targetDir, { recursive: true });
3658
3883
  copyFileSync(shippedPath, target);
3659
3884
  return existed ? "updated" : "installed";
@@ -3669,23 +3894,23 @@ function registerMcp() {
3669
3894
  encoding: "utf8"
3670
3895
  });
3671
3896
  if (res.status === 0) {
3672
- console.log(chalk21.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
3897
+ console.log(chalk22.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
3673
3898
  return;
3674
3899
  }
3675
3900
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
3676
3901
  if (out.includes("already exists")) {
3677
- console.log(chalk21.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
3902
+ console.log(chalk22.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
3678
3903
  return;
3679
3904
  }
3680
- console.error(chalk21.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
3681
- console.error(chalk21.yellow(` register it manually: ${MCP_ADD_HINT}`));
3905
+ console.error(chalk22.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
3906
+ console.error(chalk22.yellow(` register it manually: ${MCP_ADD_HINT}`));
3682
3907
  }
3683
3908
  async function agentSetup(opts) {
3684
3909
  const moduleDir = dirname(fileURLToPath(import.meta.url));
3685
3910
  const shipped = resolveShippedSkillPath(moduleDir);
3686
3911
  if (!shipped) {
3687
3912
  console.error(
3688
- chalk21.red(
3913
+ chalk22.red(
3689
3914
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
3690
3915
  )
3691
3916
  );
@@ -3694,9 +3919,9 @@ async function agentSetup(opts) {
3694
3919
  }
3695
3920
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
3696
3921
  const result = installSkill(shipped, targetDir);
3697
- console.log(chalk21.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
3922
+ console.log(chalk22.green(`\u2713 vibeview-agent skill ${result}: ${join3(targetDir, "SKILL.md")}`));
3698
3923
  console.log(
3699
- chalk21.dim(
3924
+ chalk22.dim(
3700
3925
  opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
3701
3926
  )
3702
3927
  );
@@ -3704,7 +3929,7 @@ async function agentSetup(opts) {
3704
3929
  if (!claudeCliAvailable()) {
3705
3930
  if (opts.mcp === true) {
3706
3931
  console.error(
3707
- chalk21.yellow(`\u26A0 claude CLI not found \u2014 install Claude Code, then run: ${MCP_ADD_HINT}`)
3932
+ chalk22.yellow(`\u26A0 claude CLI not found \u2014 install Claude Code, then run: ${MCP_ADD_HINT}`)
3708
3933
  );
3709
3934
  }
3710
3935
  } else if (opts.mcp === true || process.stdin.isTTY && await confirm({
@@ -3716,7 +3941,7 @@ async function agentSetup(opts) {
3716
3941
  }
3717
3942
  console.log(
3718
3943
  `
3719
- Suggested line for your project's ${chalk21.bold("CLAUDE.md")} (or ${chalk21.bold(
3944
+ Suggested line for your project's ${chalk22.bold("CLAUDE.md")} (or ${chalk22.bold(
3720
3945
  "AGENTS.md"
3721
3946
  )} for other agents):
3722
3947
  `
@@ -3724,17 +3949,17 @@ Suggested line for your project's ${chalk21.bold("CLAUDE.md")} (or ${chalk21.bol
3724
3949
  console.log(` ${CLAUDE_MD_SUGGESTION}
3725
3950
  `);
3726
3951
  console.log(
3727
- chalk21.dim(
3952
+ chalk22.dim(
3728
3953
  "Other agents (Cursor, Copilot, Codex, ...): install the same skill from GitHub with\n npx skills add vibeview/skills\nor point them at the skill file above, or add the MCP server per client \u2014\nsee https://vibeview.io/docs/agent-control"
3729
3954
  )
3730
3955
  );
3731
3956
  }
3732
3957
 
3733
3958
  // src/lib/update-check.ts
3734
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
3959
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "fs";
3735
3960
  import { homedir as homedir2 } from "os";
3736
- import { dirname as dirname2, join as join3 } from "path";
3737
- var CACHE_FILE = join3(homedir2(), ".vibeview", "update-check.json");
3961
+ import { dirname as dirname2, join as join4 } from "path";
3962
+ var CACHE_FILE = join4(homedir2(), ".vibeview", "update-check.json");
3738
3963
  var REGISTRY_URL = "https://registry.npmjs.org/vibeview/latest";
3739
3964
  var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
3740
3965
  function isNewerVersion(a, b) {
@@ -3765,7 +3990,7 @@ function updateNotice(current, latest) {
3765
3990
  }
3766
3991
  function readCache(file) {
3767
3992
  try {
3768
- const parsed = JSON.parse(readFileSync3(file, "utf8"));
3993
+ const parsed = JSON.parse(readFileSync4(file, "utf8"));
3769
3994
  if (typeof parsed?.checked_at === "number" && typeof parsed?.latest === "string") {
3770
3995
  return parsed;
3771
3996
  }
@@ -3848,11 +4073,14 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
3848
4073
  "Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
3849
4074
  ).option(
3850
4075
  "--platform <platform>",
3851
- "Run the suite on another platform (ios, android, tvos, androidtv): runs your app there with the same bundle id / package"
4076
+ "Run the suite on another platform (ios, android, tvos, androidtv): runs your linked app on that platform, else the app with the same bundle id / package"
3852
4077
  ).option(
3853
4078
  "--app <app-id>",
3854
4079
  "Run the suite against this app instead of its own (e.g. another platform build with a different bundle id)"
3855
- ).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)").action((suiteId, opts) => {
4080
+ ).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(
4081
+ "--retry",
4082
+ "Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
4083
+ ).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((suiteId, opts) => {
3856
4084
  if (opts.device && opts.deviceCategory) {
3857
4085
  process.stderr.write("error: --device cannot be combined with --device-category\n");
3858
4086
  process.exit(2);
@@ -3880,7 +4108,7 @@ program.command("run-suites").description("Run all suites matching a tag filter"
3880
4108
  []
3881
4109
  ).option("--match <mode>", "any (default) or all", "any").option(
3882
4110
  "--platform <platform>",
3883
- "Filter matched suites to one platform: ios, android, tvos, androidtv (default: no filter)"
4111
+ "Filter matched suites to one platform: ios, android, tvos, androidtv, roku (default: no filter)"
3884
4112
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-suite progress", false).option("--timeout <seconds>", "Per-suite timeout, capped at 2 hours total", "1800").option("--commit-sha <sha>", "Git commit SHA propagated to every SuiteRun").option("--model <model>", "AI model").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
3885
4113
  "--device-category <category>",
3886
4114
  "Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
@@ -3888,13 +4116,16 @@ program.command("run-suites").description("Run all suites matching a tag filter"
3888
4116
  ).option(
3889
4117
  "--device <device_id>",
3890
4118
  "Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
3891
- ).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)").action((opts) => {
4119
+ ).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(
4120
+ "--retry",
4121
+ "Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
4122
+ ).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((opts) => {
3892
4123
  if (opts.device && opts.deviceCategory) {
3893
4124
  process.stderr.write("error: --device cannot be combined with --device-category\n");
3894
4125
  process.exit(2);
3895
4126
  }
3896
4127
  try {
3897
- parseRunPlatform(opts.platform);
4128
+ parseSuitePlatformFilter(opts.platform);
3898
4129
  } catch (err) {
3899
4130
  process.stderr.write(`error: ${err.message}
3900
4131
  `);
@@ -3917,11 +4148,14 @@ program.command("run-test <test-id>").description("Run a single test case").opti
3917
4148
  "Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
3918
4149
  ).option(
3919
4150
  "--platform <platform>",
3920
- "Run the test on another platform (ios, android, tvos, androidtv): runs your app there with the same bundle id / package"
4151
+ "Run the test on another platform (ios, android, tvos, androidtv): runs your linked app on that platform, else the app with the same bundle id / package"
3921
4152
  ).option(
3922
4153
  "--app <app-id>",
3923
4154
  "Run the test against this app instead of its own (e.g. another platform build with a different bundle id)"
3924
- ).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)").action((testId, opts) => {
4155
+ ).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(
4156
+ "--retry",
4157
+ "Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
4158
+ ).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((testId, opts) => {
3925
4159
  if (opts.device && opts.deviceCategory) {
3926
4160
  process.stderr.write("error: --device cannot be combined with --device-category\n");
3927
4161
  process.exit(2);
@@ -3969,12 +4203,18 @@ program.command("list-devices").description("List devices registered with the pl
3969
4203
  program.command("upload-app <file>").description(
3970
4204
  "Upload an app build (zipped simulator .app for iOS/tvOS, .apk for Android/Android TV, or a Roku channel .zip)"
3971
4205
  ).option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--name <name>", "App name (auto-detected if not provided)").option("--note <text>", "Note stored on the uploaded build (shown in the dashboard)").action((file, opts) => uploadApp(file, opts));
3972
- program.command("create-test [name]").description("Create a standalone AI test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--app <app>", "App ID (public_id) or package name (requires --platform)").option("--platform <platform>", "Platform (ios, android) when using package name for --app").option("--steps <json>", "Test steps as inline JSON array").option("--steps-file <path>", "Path to JSON file containing test steps").option("--context <text>", "Additional context for AI test execution").option("--test-name <name>", "Test case name (alternative to positional arg)").addOption(
4206
+ program.command("create-test [name]").description("Create an AI test case, optionally in one or more groups").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--app <app>", "App ID (public_id) or package name (requires --platform)").option("--platform <platform>", "Platform (ios, android) when using package name for --app").option("--steps <json>", "Test steps as inline JSON array").option("--steps-file <path>", "Path to JSON file containing test steps").option("--context <text>", "Additional context for AI test execution").option("--test-name <name>", "Test case name (alternative to positional arg)").option(
4207
+ "--group <id...>",
4208
+ "Group (suite) ID to add the test to; repeat or list several. Same app as --app"
4209
+ ).addOption(
3973
4210
  new Option(
3974
4211
  "--if-exists <mode>",
3975
- "When a standalone test with this name already exists for the app: fail, update it, or create a new one alongside it. Default: reuse an identical test, refuse a different one"
4212
+ "When a test with this name already exists for the app (outside any group, or in one of the --group groups): fail, update it, or create a new one alongside it. Default: reuse an identical test, refuse a different one"
3976
4213
  ).choices(["fail", "update", "new"])
3977
4214
  ).action((name, opts) => createTest({ ...opts, testName: name || opts.testName || "CLI Test" }));
4215
+ program.command("import-maestro <path>").description(
4216
+ "Import Maestro flows as tests: a flow file, a .zip of your flows folder, or the folder itself"
4217
+ ).option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--app <app>", "App ID (public_id) or package name (requires --platform)").option("--platform <platform>", "Platform (ios, android) when using package name for --app").option("--group <group>", "Put the imported tests in this group (name or id)").option("--dry-run", "Show what would be imported and every flagged command; save nothing", false).action((path, opts) => importMaestro(path, opts));
3978
4218
  program.command("login").description("Log in to VibeView (interactive)").option("--base-url <url>", "VibeView server URL (defaults to https://vibeview.io)").action((opts) => login(opts));
3979
4219
  program.command("logout").description("Revoke the saved login token and remove it from ~/.vibeview/config.json").action(() => logout());
3980
4220
  program.command("dev").description(
@@ -4147,7 +4387,7 @@ program.command("dev-reload").description(
4147
4387
  program.command("mcp").description(
4148
4388
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4149
4389
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4150
- const { runMcpServer } = await import("./server-DF3CDWL4.js");
4390
+ const { runMcpServer } = await import("./server-GEY4WZUH.js");
4151
4391
  await runMcpServer({ ...opts, version: pkg.version });
4152
4392
  });
4153
4393
  program.command("agent-setup").description(