vibeview 0.29.0 → 0.31.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,7 @@ import {
15
15
  listBuilds,
16
16
  loadDevConfig,
17
17
  newTransitionState,
18
+ platformLabel,
18
19
  platformWithArticle,
19
20
  pressButtonsHelp,
20
21
  printOutcomeLine,
@@ -22,7 +23,7 @@ import {
22
23
  readDevState,
23
24
  readMetroState,
24
25
  runAgentVerb
25
- } from "./chunk-T6EQQV6E.js";
26
+ } from "./chunk-P6UIJALK.js";
26
27
  import {
27
28
  ApiClient,
28
29
  ApiStatusError,
@@ -43,14 +44,14 @@ import {
43
44
  resolveAuth,
44
45
  saveConfig,
45
46
  uploadApp
46
- } from "./chunk-PBMB6TG4.js";
47
+ } from "./chunk-ZZK7YQSP.js";
47
48
 
48
49
  // src/cli.ts
49
50
  import { createRequire } from "module";
50
51
  import { Command, Option } from "commander";
51
52
 
52
53
  // src/commands/run-suite.ts
53
- import chalk3 from "chalk";
54
+ import chalk4 from "chalk";
54
55
 
55
56
  // src/lib/poll.ts
56
57
  import chalk2 from "chalk";
@@ -144,6 +145,10 @@ function formatHuman(result, verbose) {
144
145
  lines.push(`
145
146
  ${chalk.red("Error:")} ${result.error_message}`);
146
147
  }
148
+ if (result.build_warning && result.status !== "passed") {
149
+ lines.push(`
150
+ ${chalk.yellow("Note:")} ${result.build_warning}`);
151
+ }
147
152
  lines.push("");
148
153
  return lines.join("\n");
149
154
  }
@@ -173,9 +178,16 @@ function formatJunit(result) {
173
178
  lines.push(
174
179
  ` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}" timestamp="${timestamp}">`
175
180
  );
176
- if (result.url) {
181
+ if (result.url || result.build_warning) {
177
182
  lines.push(" <properties>");
178
- lines.push(` <property name="vibeview.run_url" value="${escapeXml(result.url)}" />`);
183
+ if (result.url) {
184
+ lines.push(` <property name="vibeview.run_url" value="${escapeXml(result.url)}" />`);
185
+ }
186
+ if (result.build_warning) {
187
+ lines.push(
188
+ ` <property name="vibeview.build_warning" value="${escapeXml(result.build_warning)}" />`
189
+ );
190
+ }
179
191
  lines.push(" </properties>");
180
192
  }
181
193
  for (const tr of cases) {
@@ -224,46 +236,97 @@ function formatJunitError(name, message, url) {
224
236
  return lines.join("\n");
225
237
  }
226
238
  var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
239
+ function caseLines(c, classname) {
240
+ const name = escapeXml(c.name);
241
+ const head = ` <testcase name="${name}" classname="${classname}" time="${c.time}"`;
242
+ const out = c.url ? ` <system-out>${escapeXml(c.url)}</system-out>` : null;
243
+ if (c.verdict === "passed" && !out) return [`${head} />`];
244
+ const lines = [`${head}>`];
245
+ const msg = escapeXml(c.message ?? "");
246
+ if (c.verdict === "failure") {
247
+ lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
248
+ } else if (c.verdict === "error") {
249
+ lines.push(` <error message="${msg}" type="InfraError">${msg}</error>`);
250
+ } else if (c.verdict === "skipped") {
251
+ lines.push(` <skipped message="${msg}" />`);
252
+ }
253
+ if (out) lines.push(out);
254
+ lines.push(" </testcase>");
255
+ return lines;
256
+ }
227
257
  function formatMultiSuiteJunit(result, opts = {}) {
228
- const unfinished = (status) => opts.timedOut !== void 0 && !MULTI_TERMINAL.has(status);
229
- const errored = (status) => status === "error" || unfinished(status);
230
- const tests = result.suite_runs.reduce((sum, sr) => sum + sr.passed + sr.failed, 0);
231
- const failures = result.suite_runs.reduce((sum, sr) => sum + sr.failed, 0);
232
- const noSuites = opts.timedOut !== void 0 && result.suite_runs.length === 0;
233
- const totalErrors = result.suite_runs.filter((sr) => errored(sr.status)).length + (noSuites ? 1 : 0);
258
+ const unfinished = (status) => opts.timedOut !== void 0 && !MULTI_TERMINAL.has(status ?? "");
234
259
  const timestamp = result.started_at ?? (/* @__PURE__ */ new Date()).toISOString();
260
+ const suites = result.suite_runs.map((sr) => {
261
+ const suiteName = sr.suite_name || sr.suite_id;
262
+ const cases = (sr.case_runs ?? []).map((c) => {
263
+ const name = c.case_name || c.run_id;
264
+ const time = c.duration_ms ? (c.duration_ms / 1e3).toFixed(1) : "0";
265
+ const base = { name, time, url: c.url || void 0 };
266
+ switch (c.status) {
267
+ case "passed":
268
+ return { ...base, verdict: "passed" };
269
+ case "failed":
270
+ return {
271
+ ...base,
272
+ verdict: "failure",
273
+ message: c.error_message || "Test failed"
274
+ };
275
+ case "error":
276
+ return {
277
+ ...base,
278
+ verdict: "error",
279
+ message: c.error_message || "Infrastructure error"
280
+ };
281
+ case "cancelled":
282
+ return { ...base, verdict: "skipped", message: c.error_message || "Cancelled" };
283
+ default:
284
+ return unfinished(c.status) ? {
285
+ ...base,
286
+ verdict: "error",
287
+ message: `${opts.timedOut} (case was ${c.status ?? "not started"})`
288
+ } : { ...base, verdict: "skipped", message: "Not run" };
289
+ }
290
+ });
291
+ const suiteError = unfinished(sr.status) ? `${opts.timedOut} (suite was ${sr.status})` : sr.status === "error" ? sr.error_message || "Suite errored" : null;
292
+ if (suiteError && !cases.some((c) => c.verdict === "error")) {
293
+ cases.push({ name: suiteName, time: "0", verdict: "error", message: suiteError });
294
+ }
295
+ const legacy = sr.case_runs === void 0;
296
+ const tests2 = legacy ? sr.passed + sr.failed + (suiteError ? 1 : 0) : cases.length;
297
+ const failures = legacy ? sr.failed : cases.filter((c) => c.verdict === "failure").length;
298
+ const errors2 = cases.filter((c) => c.verdict === "error").length;
299
+ const skipped = cases.filter((c) => c.verdict === "skipped").length;
300
+ return { sr, suiteName, cases, tests: tests2, failures, errors: errors2, skipped };
301
+ });
302
+ const noSuites = opts.timedOut !== void 0 && result.suite_runs.length === 0;
303
+ const sum = (key) => suites.reduce((n, s) => n + s[key], 0);
304
+ const tests = sum("tests") + (noSuites ? 1 : 0);
305
+ const errors = sum("errors") + (noSuites ? 1 : 0);
235
306
  const lines = [];
236
307
  lines.push('<?xml version="1.0" encoding="UTF-8"?>');
237
308
  lines.push(
238
- `<testsuites name="vibeview-multi" tests="${tests}" failures="${failures}" errors="${totalErrors}">`
309
+ `<testsuites name="vibeview-multi" tests="${tests}" failures="${sum("failures")}" errors="${errors}" skipped="${sum("skipped")}">`
239
310
  );
240
- for (const sr of result.suite_runs) {
241
- const suiteName = escapeXml(sr.suite_name || sr.suite_id);
242
- const suiteTests = sr.passed + sr.failed;
243
- const suiteFails = sr.failed;
244
- const suiteErrors = errored(sr.status) ? 1 : 0;
311
+ for (const s of suites) {
312
+ const suiteName = escapeXml(s.suiteName);
245
313
  lines.push(
246
- ` <testsuite name="${suiteName}" tests="${suiteTests}" failures="${suiteFails}" errors="${suiteErrors}" timestamp="${timestamp}">`
314
+ ` <testsuite name="${suiteName}" tests="${s.tests}" failures="${s.failures}" errors="${s.errors}" skipped="${s.skipped}" timestamp="${timestamp}">`
247
315
  );
248
- if (sr.url) {
316
+ if (s.sr.url) {
249
317
  lines.push(" <properties>");
250
- lines.push(` <property name="vibeview.run_url" value="${escapeXml(sr.url)}" />`);
318
+ lines.push(` <property name="vibeview.run_url" value="${escapeXml(s.sr.url)}" />`);
251
319
  lines.push(" </properties>");
252
320
  }
253
- if (unfinished(sr.status)) {
254
- const msg = escapeXml(`${opts.timedOut} (suite was ${sr.status})`);
255
- lines.push(` <error message="${msg}" type="InfraError" />`);
256
- } else if (suiteErrors > 0) {
257
- lines.push(` <error message="Suite errored" type="InfraError" />`);
258
- }
321
+ for (const c of s.cases) lines.push(...caseLines(c, suiteName));
259
322
  lines.push(" </testsuite>");
260
323
  }
261
324
  if (noSuites) {
262
- const msg = escapeXml(opts.timedOut);
325
+ const name = "vibeview run-suites";
263
326
  lines.push(
264
- ` <testsuite name="vibeview run-suites" tests="0" failures="0" errors="1" timestamp="${timestamp}">`
327
+ ` <testsuite name="${name}" tests="1" failures="0" errors="1" skipped="0" timestamp="${timestamp}">`
265
328
  );
266
- lines.push(` <error message="${msg}" type="InfraError" />`);
329
+ lines.push(...caseLines({ name, time: "0", verdict: "error", message: opts.timedOut }, name));
267
330
  lines.push(" </testsuite>");
268
331
  }
269
332
  lines.push("</testsuites>");
@@ -570,6 +633,42 @@ function mapExitCode(status) {
570
633
  return EXIT_INFRA_ERROR;
571
634
  }
572
635
 
636
+ // src/lib/run-target.ts
637
+ import chalk3 from "chalk";
638
+ var RUN_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
639
+ function parseRunPlatform(value) {
640
+ if (value === void 0) return void 0;
641
+ if (!RUN_PLATFORMS.includes(value)) {
642
+ throw new Error(`--platform must be one of ${RUN_PLATFORMS.join(", ")}, got "${value}"`);
643
+ }
644
+ return value;
645
+ }
646
+ function refusePlatformWithApp(options) {
647
+ if (options.platform && options.app) {
648
+ throw new Error(
649
+ "--platform and --app cannot be combined: --app already names the app, and so the platform, the run uses. Pass --app <id> to pick an app, or --platform to run the app with the same bundle id / package on that platform."
650
+ );
651
+ }
652
+ }
653
+ function printBuildWarning(warning, label) {
654
+ if (!warning) return;
655
+ const prefix = label ? `warning (${label}):` : "warning:";
656
+ process.stderr.write(chalk3.yellow(`${prefix} ${warning}`) + "\n");
657
+ }
658
+ function describeRunTarget(response, options) {
659
+ const asked = Boolean(options.platform || options.app);
660
+ if (!response.app_id) {
661
+ if (asked) {
662
+ throw new Error(
663
+ "this VibeView server does not support --platform/--app on test runs yet; the run was stopped rather than run on the test\u2019s own platform"
664
+ );
665
+ }
666
+ return null;
667
+ }
668
+ if (!asked) return null;
669
+ return `${response.app_id} (${platformLabel(response.platform ?? "")})`;
670
+ }
671
+
573
672
  // src/commands/run-suite.ts
574
673
  async function runSuite(suiteId, options) {
575
674
  const { token, baseUrl } = resolveAuth(options);
@@ -635,17 +734,22 @@ ${signal} received.
635
734
  deviceCategory: options.deviceCategory,
636
735
  device: options.device,
637
736
  build: options.build,
638
- metadata: options.metadata
737
+ metadata: options.metadata,
738
+ platform: options.platform,
739
+ app: options.app
639
740
  });
640
741
  sessionId = response.session_id;
641
742
  runId = response.run_id;
743
+ const target = describeRunTarget(response, options);
642
744
  spinner.stop();
643
745
  process.stderr.write(
644
- `${chalk3.dim("Suite:")} ${suiteId}
645
- ${chalk3.dim("Run:")} ${response.run_id}
646
- ` + (sessionId ? `${chalk3.dim("Session:")} ${sessionId}
746
+ `${chalk4.dim("Suite:")} ${suiteId}
747
+ ` + (target ? `${chalk4.dim("App:")} ${target}
748
+ ` : "") + `${chalk4.dim("Run:")} ${response.run_id}
749
+ ` + (sessionId ? `${chalk4.dim("Session:")} ${sessionId}
647
750
  ` : "")
648
751
  );
752
+ printBuildWarning(response.build_warning);
649
753
  spinner.start("Waiting for device...");
650
754
  const timeoutMs = (options.timeout ?? 300) * 1e3;
651
755
  const result = await pollUntilComplete(
@@ -663,7 +767,10 @@ ${chalk3.dim("Run:")} ${response.run_id}
663
767
  process.stdin.setRawMode(false);
664
768
  process.stdin.pause();
665
769
  }
666
- outputResult(result, options);
770
+ outputResult(
771
+ response.build_warning ? { ...result, build_warning: response.build_warning } : result,
772
+ options
773
+ );
667
774
  process.exit(mapExitCode(result.status));
668
775
  } catch (err) {
669
776
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
@@ -689,7 +796,7 @@ ${chalk3.dim("Run:")} ${response.run_id}
689
796
  }
690
797
 
691
798
  // src/commands/run-suites.ts
692
- import chalk4 from "chalk";
799
+ import chalk5 from "chalk";
693
800
  var AGGREGATE_TIMEOUT_CAP_S = 7200;
694
801
  async function runSuites(opts) {
695
802
  if (!opts.tag || opts.tag.length === 0) {
@@ -764,13 +871,13 @@ ${signal} received.
764
871
  const noMatch = `No suites match tags: ${opts.tag.join(", ")}${suffix}`;
765
872
  if (opts.output === "json") printJsonError(noMatch);
766
873
  if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", noMatch));
767
- process.stderr.write(chalk4.yellow(`${noMatch}
874
+ process.stderr.write(chalk5.yellow(`${noMatch}
768
875
  `));
769
876
  process.exit(EXIT_INFRA_ERROR);
770
877
  }
771
878
  if (opts.output === "json") printJsonError(err);
772
879
  if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", msg));
773
- process.stderr.write(chalk4.red(`Failed to start: ${msg}
880
+ process.stderr.write(chalk5.red(`Failed to start: ${msg}
774
881
  `));
775
882
  process.exit(EXIT_INFRA_ERROR);
776
883
  }
@@ -779,16 +886,17 @@ ${signal} received.
779
886
  const matchedNames = start.matched_suites?.map((s) => s.name) ?? [];
780
887
  const totalMatched = start.matched_suite_ids.length;
781
888
  process.stderr.write(
782
- `${chalk4.dim("Aggregate:")} ${start.aggregate_id}
783
- ${chalk4.dim("Tags:")} ${opts.tag.join(", ")} ${chalk4.dim("(" + (opts.match ?? "any") + ")")}
784
- ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u2014 " + matchedNames.join(", ") : ""}
889
+ `${chalk5.dim("Aggregate:")} ${start.aggregate_id}
890
+ ${chalk5.dim("Tags:")} ${opts.tag.join(", ")} ${chalk5.dim("(" + (opts.match ?? "any") + ")")}
891
+ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u2014 " + matchedNames.join(", ") : ""}
785
892
  `
786
893
  );
894
+ for (const s of start.matched_suites ?? []) printBuildWarning(s.build_warning, s.name);
787
895
  const excluded = start.excluded_suites ?? [];
788
896
  if (excluded.length) {
789
897
  const names = excluded.map((s) => s.name).join(", ");
790
898
  process.stderr.write(
791
- chalk4.dim(`Excluded ${excluded.length} suite(s) (platform mismatch): ${names}
899
+ chalk5.dim(`Excluded ${excluded.length} suite(s) (platform mismatch): ${names}
792
900
  `)
793
901
  );
794
902
  }
@@ -802,20 +910,28 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
802
910
  const suiteStateBefore = /* @__PURE__ */ new Map();
803
911
  const suiteStartedPrinted = /* @__PURE__ */ new Set();
804
912
  let lastQueueReason = null;
913
+ let pending = [];
805
914
  const print = (line) => {
915
+ pending.push(line);
916
+ };
917
+ const flush = (nextText) => {
918
+ const textChanged = nextText !== null && nextText !== spinner.text;
919
+ if (pending.length === 0 && !textChanged) return;
806
920
  spinner.stop();
807
- process.stderr.write(line + "\n");
808
- spinner.start();
921
+ for (const line of pending) process.stderr.write(line + "\n");
922
+ pending = [];
923
+ if (nextText !== null) spinner.start(nextText);
809
924
  };
810
925
  const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
811
926
  let last = null;
812
927
  const abandon = async (reason) => {
928
+ flush(null);
813
929
  spinner.stop();
814
930
  if (process.stdin.isTTY) {
815
931
  process.stdin.setRawMode(false);
816
932
  process.stdin.pause();
817
933
  }
818
- process.stderr.write(chalk4.red(`${reason}
934
+ process.stderr.write(chalk5.red(`${reason}
819
935
  `));
820
936
  process.stderr.write("Cancelling multi-suite run...\n");
821
937
  try {
@@ -823,7 +939,7 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
823
939
  process.stderr.write("Cancellation requested.\n");
824
940
  } catch (err) {
825
941
  const m = err instanceof Error ? err.message : String(err);
826
- process.stderr.write(chalk4.yellow(`Could not cancel the run: ${m}
942
+ process.stderr.write(chalk5.yellow(`Could not cancel the run: ${m}
827
943
  `));
828
944
  }
829
945
  if (opts.output === "json") {
@@ -851,6 +967,7 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
851
967
  return abandon(`Could not read the run's status: ${m}`);
852
968
  }
853
969
  let doneCount = 0;
970
+ let nextText;
854
971
  let queuedChild;
855
972
  for (const sr of last.suite_runs) {
856
973
  const cur = sr.status;
@@ -860,17 +977,17 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
860
977
  }
861
978
  if (cur === "running" && !suiteStartedPrinted.has(sr.run_id)) {
862
979
  suiteStartedPrinted.add(sr.run_id);
863
- print(`${chalk4.cyan("\u25B6")} ${chalk4.bold(sr.suite_name)} started`);
980
+ print(`${chalk5.cyan("\u25B6")} ${chalk5.bold(sr.suite_name)} started`);
864
981
  }
865
982
  const wasTerminal = prev !== void 0 && TERMINAL.has(prev);
866
983
  if (TERMINAL.has(cur) && !wasTerminal) {
867
- const mark = cur === "passed" ? chalk4.green("\u2713") : cur === "cancelled" ? chalk4.yellow("\u26A0") : chalk4.red("\u2717");
868
- const link = sr.url ? chalk4.dim(" " + sr.url) : "";
984
+ const mark = cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
985
+ const link = sr.url ? chalk5.dim(" " + sr.url) : "";
869
986
  print(
870
- `${mark} ${chalk4.bold(sr.suite_name)} ${cur}: ${sr.passed} passed, ${sr.failed} failed${link}`
987
+ `${mark} ${chalk5.bold(sr.suite_name)} ${cur}: ${sr.passed} passed, ${sr.failed} failed${link}`
871
988
  );
872
989
  if (sr.error_message) {
873
- print(` ${chalk4.red(sr.error_message)}`);
990
+ print(` ${chalk5.red(sr.error_message)}`);
874
991
  }
875
992
  }
876
993
  suiteStateBefore.set(sr.run_id, cur);
@@ -883,16 +1000,20 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
883
1000
  const why = queueReasonCopy(q.reason);
884
1001
  if (why && why !== lastQueueReason) {
885
1002
  print(
886
- `${chalk4.yellow("\u23F3")} ${chalk4.bold(queuedChild.suite_name)} is waiting for a device: ${why}`
1003
+ `${chalk5.yellow("\u23F3")} ${chalk5.bold(queuedChild.suite_name)} is waiting for a device: ${why}`
887
1004
  );
888
1005
  }
889
1006
  lastQueueReason = why;
890
- spinner.text = `Queued \u2014 ${chalk4.bold(queuedChild.suite_name)} position ${q.position} for ${target} (${doneCount}/${totalMatched} suite(s) complete)`;
1007
+ nextText = `Queued \u2014 ${chalk5.bold(queuedChild.suite_name)} position ${q.position} for ${target} (${doneCount}/${totalMatched} suite(s) complete)`;
891
1008
  } else {
892
1009
  lastQueueReason = null;
893
- spinner.text = `Running suites \u2014 ${doneCount}/${totalMatched} complete...`;
1010
+ nextText = `Running suites \u2014 ${doneCount}/${totalMatched} complete...`;
1011
+ }
1012
+ if (TERMINAL.has(last.status)) {
1013
+ flush(null);
1014
+ break;
894
1015
  }
895
- if (TERMINAL.has(last.status)) break;
1016
+ flush(nextText);
896
1017
  await new Promise((r) => setTimeout(r, 2e3));
897
1018
  }
898
1019
  if (!last || !TERMINAL.has(last.status)) {
@@ -906,7 +1027,9 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
906
1027
  process.stdin.pause();
907
1028
  }
908
1029
  if (opts.output === "json") {
909
- process.stdout.write(JSON.stringify(last, null, 2) + "\n");
1030
+ const buildWarnings = (start.matched_suites ?? []).filter((s) => s.build_warning).map((s) => ({ suite_id: s.id, suite_name: s.name, build_warning: s.build_warning }));
1031
+ const doc = buildWarnings.length ? { ...last, build_warnings: buildWarnings } : last;
1032
+ process.stdout.write(JSON.stringify(doc, null, 2) + "\n");
910
1033
  } else if (opts.output === "junit") {
911
1034
  process.stdout.write(formatMultiSuiteJunit(last) + "\n");
912
1035
  } else {
@@ -914,25 +1037,25 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
914
1037
  Aggregate: ${last.status}
915
1038
  `);
916
1039
  for (const sr of last.suite_runs) {
917
- const mark = sr.status === "passed" ? chalk4.green("\u2713") : chalk4.red("\u2717");
918
- const link = sr.url ? chalk4.dim(` ${sr.url}`) : "";
1040
+ const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
1041
+ const link = sr.url ? chalk5.dim(` ${sr.url}`) : "";
919
1042
  process.stdout.write(
920
1043
  ` ${mark} ${sr.suite_name}: ${sr.passed} passed, ${sr.failed} failed${link}
921
1044
  `
922
1045
  );
923
1046
  if (sr.error_message) {
924
- process.stdout.write(` ${chalk4.red(sr.error_message)}
1047
+ process.stdout.write(` ${chalk5.red(sr.error_message)}
925
1048
  `);
926
1049
  }
927
1050
  for (const cr of sr.case_runs ?? []) {
928
- const caseMark = cr.status === "passed" ? chalk4.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk4.yellow("-") : chalk4.red("\u2717");
929
- const dur = cr.duration_ms ? chalk4.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
1051
+ const caseMark = cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
1052
+ const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
930
1053
  process.stdout.write(
931
- ` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${dur} ${chalk4.dim(cr.url)}
1054
+ ` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${dur} ${chalk5.dim(cr.url)}
932
1055
  `
933
1056
  );
934
1057
  if (cr.error_message && cr.status !== "passed") {
935
- process.stdout.write(` ${chalk4.red(cr.error_message)}
1058
+ process.stdout.write(` ${chalk5.red(cr.error_message)}
936
1059
  `);
937
1060
  }
938
1061
  }
@@ -944,7 +1067,7 @@ Aggregate: ${last.status}
944
1067
  }
945
1068
 
946
1069
  // src/commands/run-test.ts
947
- import chalk5 from "chalk";
1070
+ import chalk6 from "chalk";
948
1071
  async function runTest(testId, options) {
949
1072
  const { token, baseUrl } = resolveAuth(options);
950
1073
  const client = new ApiClient(baseUrl, token);
@@ -1009,18 +1132,23 @@ ${signal} received.
1009
1132
  deviceCategory: options.deviceCategory,
1010
1133
  device: options.device,
1011
1134
  build: options.build,
1012
- metadata: options.metadata
1135
+ metadata: options.metadata,
1136
+ platform: options.platform,
1137
+ app: options.app
1013
1138
  });
1014
1139
  sessionId = response.session_id;
1015
1140
  runId = response.run_id;
1141
+ const target = describeRunTarget(response, options);
1016
1142
  spinner.stop();
1017
1143
  process.stderr.write(
1018
- `${chalk5.dim("Session:")} ${sessionId || "unknown"}
1019
- ${chalk5.dim("Device:")} ${response.device_name || "allocating..."}
1020
- ${chalk5.dim("Test:")} ${testId}
1021
- ${chalk5.dim("Run:")} ${response.run_id}
1144
+ `${chalk6.dim("Session:")} ${sessionId || "unknown"}
1145
+ ${chalk6.dim("Device:")} ${response.device_name || "allocating..."}
1146
+ ${chalk6.dim("Test:")} ${testId}
1147
+ ` + (target ? `${chalk6.dim("App:")} ${target}
1148
+ ` : "") + `${chalk6.dim("Run:")} ${response.run_id}
1022
1149
  `
1023
1150
  );
1151
+ printBuildWarning(response.build_warning);
1024
1152
  spinner.start("Waiting for device...");
1025
1153
  const timeoutMs = (options.timeout ?? 300) * 1e3;
1026
1154
  const result = await pollUntilComplete(
@@ -1038,7 +1166,10 @@ ${chalk5.dim("Run:")} ${response.run_id}
1038
1166
  process.stdin.setRawMode(false);
1039
1167
  process.stdin.pause();
1040
1168
  }
1041
- outputResult(result, options);
1169
+ outputResult(
1170
+ response.build_warning ? { ...result, build_warning: response.build_warning } : result,
1171
+ options
1172
+ );
1042
1173
  process.exit(mapExitCode(result.status));
1043
1174
  } catch (err) {
1044
1175
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
@@ -1064,7 +1195,7 @@ ${chalk5.dim("Run:")} ${response.run_id}
1064
1195
  }
1065
1196
 
1066
1197
  // src/commands/list-suites.ts
1067
- import chalk6 from "chalk";
1198
+ import chalk7 from "chalk";
1068
1199
  async function listSuites(options) {
1069
1200
  const { token, baseUrl } = resolveAuth(options);
1070
1201
  const client = new ApiClient(baseUrl, token);
@@ -1086,21 +1217,21 @@ async function listSuites(options) {
1086
1217
  console.log("No test suites found.");
1087
1218
  return;
1088
1219
  }
1089
- console.log(chalk6.bold(`
1220
+ console.log(chalk7.bold(`
1090
1221
  ${suites.length} suite(s)
1091
1222
  `));
1092
1223
  for (const s of suites) {
1093
- const platform = s.platform ? chalk6.dim(` [${s.platform}]`) : "";
1094
- const tagsText = s.tags && s.tags.length > 0 ? chalk6.dim(` [${s.tags.map((t) => t.display_name).join(", ")}]`) : "";
1224
+ const platform = s.platform ? chalk7.dim(` [${s.platform}]`) : "";
1225
+ const tagsText = s.tags && s.tags.length > 0 ? chalk7.dim(` [${s.tags.map((t) => t.display_name).join(", ")}]`) : "";
1095
1226
  console.log(
1096
- ` ${chalk6.cyan(s.id)} ${s.name}${platform}${tagsText} (${s.case_count} ${s.case_count === 1 ? "case" : "cases"})`
1227
+ ` ${chalk7.cyan(s.id)} ${s.name}${platform}${tagsText} (${s.case_count} ${s.case_count === 1 ? "case" : "cases"})`
1097
1228
  );
1098
1229
  }
1099
1230
  console.log();
1100
1231
  }
1101
1232
 
1102
1233
  // src/commands/list-apps.ts
1103
- import chalk7 from "chalk";
1234
+ import chalk8 from "chalk";
1104
1235
  async function listApps(options) {
1105
1236
  const { token, baseUrl } = resolveAuth(options);
1106
1237
  const client = new ApiClient(baseUrl, token);
@@ -1119,20 +1250,20 @@ async function listApps(options) {
1119
1250
  console.log("No apps found.");
1120
1251
  return;
1121
1252
  }
1122
- console.log(chalk7.bold(`
1253
+ console.log(chalk8.bold(`
1123
1254
  ${apps.length} app(s)
1124
1255
  `));
1125
1256
  for (const app of apps) {
1126
- const platform = chalk7.dim(` [${app.platform}]`);
1127
- const pkg2 = chalk7.dim(app.package_name);
1128
- console.log(` ${chalk7.cyan(app.public_id)} ${app.name}${platform} ${pkg2}`);
1129
- console.log(chalk7.dim(` ${baseUrl}/apps/${app.public_id}`));
1257
+ const platform = chalk8.dim(` [${app.platform}]`);
1258
+ const pkg2 = chalk8.dim(app.package_name);
1259
+ console.log(` ${chalk8.cyan(app.public_id)} ${app.name}${platform} ${pkg2}`);
1260
+ console.log(chalk8.dim(` ${baseUrl}/apps/${app.public_id}`));
1130
1261
  }
1131
1262
  console.log();
1132
1263
  }
1133
1264
 
1134
1265
  // src/commands/stop.ts
1135
- import chalk8 from "chalk";
1266
+ import chalk9 from "chalk";
1136
1267
  async function stopCommand(sessionId, opts, deps) {
1137
1268
  const id = sessionId?.trim();
1138
1269
  if (!id) throw new UsageError("a session id is required: vibeview stop <session-id>");
@@ -1143,7 +1274,7 @@ async function stopCommand(sessionId, opts, deps) {
1143
1274
  } catch (err) {
1144
1275
  const message = err instanceof Error ? err.message : String(err);
1145
1276
  if (opts.json) console.log(JSON.stringify({ stopped: false, session_id: id, message }));
1146
- console.error(chalk8.red(`error: ${message}`));
1277
+ console.error(chalk9.red(`error: ${message}`));
1147
1278
  process.exitCode = errorExitCode(err);
1148
1279
  return;
1149
1280
  }
@@ -1176,7 +1307,7 @@ async function stopCommand(sessionId, opts, deps) {
1176
1307
  }
1177
1308
 
1178
1309
  // src/commands/create-test.ts
1179
- import chalk9 from "chalk";
1310
+ import chalk10 from "chalk";
1180
1311
  import { existsSync, readFileSync } from "fs";
1181
1312
  async function readStdin() {
1182
1313
  return new Promise((resolve, reject) => {
@@ -1217,6 +1348,18 @@ async function resolveSteps(options) {
1217
1348
  }
1218
1349
  throw new Error("No steps provided. Use --steps, --steps-file, or pipe JSON to stdin.");
1219
1350
  }
1351
+ function assertHasSteps(steps) {
1352
+ if (!Array.isArray(steps)) {
1353
+ throw new Error(
1354
+ `Steps must be a JSON array of instructions, e.g. '["Open the app", "Verify the Home screen shows"]'.`
1355
+ );
1356
+ }
1357
+ if (steps.length === 0) {
1358
+ throw new Error(
1359
+ `A test needs at least one step. Pass a JSON array with one or more instructions, e.g. --steps '["Verify the Home screen shows a list of trips"]'.`
1360
+ );
1361
+ }
1362
+ }
1220
1363
  async function resolveAppId(client, appRef, platform) {
1221
1364
  if (appRef.includes(".")) {
1222
1365
  if (!platform) {
@@ -1238,6 +1381,7 @@ async function createTest(options) {
1238
1381
  const client = new ApiClient(baseUrl, token);
1239
1382
  try {
1240
1383
  const steps = await resolveSteps(options);
1384
+ assertHasSteps(steps);
1241
1385
  let appPublicId;
1242
1386
  if (options.app) {
1243
1387
  appPublicId = await resolveAppId(client, options.app, options.platform);
@@ -1271,20 +1415,20 @@ async function createTest(options) {
1271
1415
  );
1272
1416
  } else {
1273
1417
  console.log(`
1274
- ${chalk9.cyan(result.public_id)} ${result.name}`);
1418
+ ${chalk10.cyan(result.public_id)} ${result.name}`);
1275
1419
  if (result.app_name) {
1276
- console.log(` ${chalk9.dim(`App: ${result.app_name}`)}`);
1420
+ console.log(` ${chalk10.dim(`App: ${result.app_name}`)}`);
1277
1421
  }
1278
1422
  if (result.platform) {
1279
- console.log(` ${chalk9.dim(`Platform: ${result.platform}`)}`);
1423
+ console.log(` ${chalk10.dim(`Platform: ${result.platform}`)}`);
1280
1424
  }
1281
- console.log(` ${chalk9.dim(`Steps: ${steps.length}`)}`);
1282
- console.log(chalk9.dim(` ${baseUrl}/tests?tab=standalone
1425
+ console.log(` ${chalk10.dim(`Steps: ${steps.length}`)}`);
1426
+ console.log(chalk10.dim(` ${baseUrl}/tests?tab=standalone
1283
1427
  `));
1284
1428
  }
1285
1429
  } catch (err) {
1286
1430
  if (options.output === "json") printJsonError(err);
1287
- console.error(chalk9.red(err instanceof Error ? err.message : "Failed to create test"));
1431
+ console.error(chalk10.red(err instanceof Error ? err.message : "Failed to create test"));
1288
1432
  process.exit(
1289
1433
  err instanceof ApiStatusError && err.status === 409 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR
1290
1434
  );
@@ -1368,7 +1512,7 @@ async function logout() {
1368
1512
  }
1369
1513
 
1370
1514
  // src/commands/dev-status.ts
1371
- import chalk10 from "chalk";
1515
+ import chalk11 from "chalk";
1372
1516
  function buildLine(info) {
1373
1517
  if (!info || typeof info.app_build_id !== "number") return null;
1374
1518
  const version = info.app_version ? ` version ${info.app_version}${info.app_build_number ? ` (${info.app_build_number})` : ""}` : "";
@@ -1424,7 +1568,7 @@ async function devStatus(opts) {
1424
1568
  console.error(` url: ${state.page_url}`);
1425
1569
  console.error(` started: ${state.started_at}`);
1426
1570
  }
1427
- if (metro) console.error(chalk10.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
1571
+ if (metro) console.error(chalk11.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
1428
1572
  process.exit(0);
1429
1573
  }
1430
1574
 
@@ -1460,12 +1604,12 @@ async function devStop(opts) {
1460
1604
  }
1461
1605
 
1462
1606
  // src/commands/dev-reload.ts
1463
- import chalk11 from "chalk";
1607
+ import chalk12 from "chalk";
1464
1608
  function fail(opts, code, message, exit) {
1465
1609
  if (opts.json) {
1466
1610
  console.log(JSON.stringify({ reloaded: false, code, message }));
1467
1611
  } else {
1468
- console.error(chalk11.red(`error: ${message}`));
1612
+ console.error(chalk12.red(`error: ${message}`));
1469
1613
  }
1470
1614
  process.exit(exit);
1471
1615
  }
@@ -1518,7 +1662,7 @@ async function devReload(opts) {
1518
1662
  api,
1519
1663
  // Build output and the "reloaded in" line are human output: stderr, so
1520
1664
  // stdout stays reserved for the --json result.
1521
- log: (line) => console.error(chalk11.dim(line))
1665
+ log: (line) => console.error(chalk12.dim(line))
1522
1666
  });
1523
1667
  const started = Date.now();
1524
1668
  try {
@@ -1539,10 +1683,10 @@ async function devReload(opts) {
1539
1683
  }
1540
1684
 
1541
1685
  // src/commands/builds.ts
1542
- import chalk13 from "chalk";
1686
+ import chalk14 from "chalk";
1543
1687
 
1544
1688
  // src/lib/formatters/builds.ts
1545
- import chalk12 from "chalk";
1689
+ import chalk13 from "chalk";
1546
1690
  var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
1547
1691
  var UNBILLED_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
1548
1692
  function formatCostCell(costMicro, status) {
@@ -1550,13 +1694,13 @@ function formatCostCell(costMicro, status) {
1550
1694
  return `$${(Math.max(costMicro, 0) / 1e6).toFixed(2)}`;
1551
1695
  }
1552
1696
  var STATUS_COLOR = {
1553
- succeeded: chalk12.green,
1554
- failed: chalk12.red,
1555
- timed_out: chalk12.red,
1556
- infra_failed: chalk12.red,
1557
- canceled: chalk12.dim,
1558
- running: chalk12.yellow,
1559
- queued: chalk12.dim
1697
+ succeeded: chalk13.green,
1698
+ failed: chalk13.red,
1699
+ timed_out: chalk13.red,
1700
+ infra_failed: chalk13.red,
1701
+ canceled: chalk13.dim,
1702
+ running: chalk13.yellow,
1703
+ queued: chalk13.dim
1560
1704
  };
1561
1705
  function formatBuildsHuman(builds2) {
1562
1706
  if (builds2.length === 0) {
@@ -1573,7 +1717,7 @@ function formatBuildsHuman(builds2) {
1573
1717
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1574
1718
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1575
1719
  const join4 = (cells) => cells.join(" ").trimEnd();
1576
- const lines = [chalk12.bold(join4(padCells(HEADERS)))];
1720
+ const lines = [chalk13.bold(join4(padCells(HEADERS)))];
1577
1721
  for (const row of rows) {
1578
1722
  const padded = padCells(row);
1579
1723
  const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
@@ -1632,7 +1776,7 @@ async function buildsLogs(id, opts, deps) {
1632
1776
  console.log(JSON.stringify(logs));
1633
1777
  } else {
1634
1778
  if (logs.content) process.stdout.write(logs.content);
1635
- console.error(chalk13.dim(`status: ${logs.status}`));
1779
+ console.error(chalk14.dim(`status: ${logs.status}`));
1636
1780
  }
1637
1781
  process.exitCode = 0;
1638
1782
  return;
@@ -1729,7 +1873,7 @@ async function buildsDownload(id, opts, deps) {
1729
1873
  if (opts.json) {
1730
1874
  console.log(JSON.stringify(result));
1731
1875
  } else {
1732
- console.log(chalk13.green(`\u2713 saved ${result.path}`));
1876
+ console.log(chalk14.green(`\u2713 saved ${result.path}`));
1733
1877
  }
1734
1878
  process.exitCode = 0;
1735
1879
  } catch (err) {
@@ -1746,7 +1890,7 @@ async function buildsCancel(id, opts, deps) {
1746
1890
  if (opts.json) {
1747
1891
  console.log(JSON.stringify(build));
1748
1892
  } else {
1749
- console.log(`${chalk13.cyan(build.id)} ${build.status}`);
1893
+ console.log(`${chalk14.cyan(build.id)} ${build.status}`);
1750
1894
  }
1751
1895
  process.exitCode = 0;
1752
1896
  } catch (err) {
@@ -1763,7 +1907,7 @@ async function buildsDeleteArtifact(id, opts, deps) {
1763
1907
  if (opts.json) {
1764
1908
  console.log(JSON.stringify(result));
1765
1909
  } else {
1766
- console.log(chalk13.green(`\u2713 deleted artifact for ${id}`));
1910
+ console.log(chalk14.green(`\u2713 deleted artifact for ${id}`));
1767
1911
  }
1768
1912
  process.exitCode = 0;
1769
1913
  } catch (err) {
@@ -1774,11 +1918,11 @@ async function buildsDeleteArtifact(id, opts, deps) {
1774
1918
  function printError(opts, err) {
1775
1919
  const message = err instanceof Error ? err.message : String(err);
1776
1920
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1777
- console.error(chalk13.red(`error: ${message}`));
1921
+ console.error(chalk14.red(`error: ${message}`));
1778
1922
  }
1779
1923
 
1780
1924
  // src/commands/submit.ts
1781
- import chalk14 from "chalk";
1925
+ import chalk15 from "chalk";
1782
1926
  function defaultDeps2(client) {
1783
1927
  return {
1784
1928
  createSubmission: client.createSubmission.bind(client),
@@ -1802,7 +1946,7 @@ function submitConfigFor(cwd, platform) {
1802
1946
  cfg = loadDevConfig(cwd);
1803
1947
  } catch (err) {
1804
1948
  console.error(
1805
- chalk14.yellow(
1949
+ chalk15.yellow(
1806
1950
  `warning: vibeview.json could not be read \u2014 ignoring submit defaults (${err instanceof Error ? err.message : String(err)})`
1807
1951
  )
1808
1952
  );
@@ -1950,12 +2094,12 @@ async function submitCommand(opts, deps) {
1950
2094
  return;
1951
2095
  }
1952
2096
  if (!opts.json) {
1953
- console.error(` ${chalk14.cyan(sub.id)} ${chalk14.dim(`[${sub.platform}]`)}`);
2097
+ console.error(` ${chalk15.cyan(sub.id)} ${chalk15.dim(`[${sub.platform}]`)}`);
1954
2098
  }
1955
2099
  const final = await followSubmission(sub, opts, d);
1956
2100
  if (!opts.json && final.status === "succeeded" && (final.release_status ?? releaseStatus) === "draft") {
1957
2101
  console.error(
1958
- chalk14.dim("release status: draft \u2014 activate it from the Play Console when ready")
2102
+ chalk15.dim("release status: draft \u2014 activate it from the Play Console when ready")
1959
2103
  );
1960
2104
  }
1961
2105
  process.exitCode = final.status === "succeeded" ? 0 : 1;
@@ -1986,11 +2130,11 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
1986
2130
  });
1987
2131
  emit(opts.json, { event: "preflight", ...result });
1988
2132
  if (!opts.json) {
1989
- const mark = result.ready ? chalk14.green("\u2713") : chalk14.red("\u2717");
2133
+ const mark = result.ready ? chalk15.green("\u2713") : chalk15.red("\u2717");
1990
2134
  console.error(`${mark} ${result.message}`);
1991
2135
  if (result.ready) {
1992
2136
  console.error(
1993
- chalk14.dim(
2137
+ chalk15.dim(
1994
2138
  ` next: vibeview build --cloud --production --platform ${platform}, then vibeview submit --platform ${platform} --latest`
1995
2139
  )
1996
2140
  );
@@ -2024,7 +2168,7 @@ async function linkedIdentifier(platform, opts, deps) {
2024
2168
  }
2025
2169
  if (!app.package_name) return void 0;
2026
2170
  if (!opts.json) {
2027
- console.error(chalk14.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2171
+ console.error(chalk15.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2028
2172
  }
2029
2173
  return app.package_name;
2030
2174
  }
@@ -2065,14 +2209,14 @@ async function followSubmission(created, opts, deps) {
2065
2209
  if (opts.json) {
2066
2210
  emit(opts.json, { event: "status", id: created.id, status: "queued" });
2067
2211
  } else {
2068
- console.error(chalk14.dim("\u23F3 queued \u2014 waiting for a submission slot"));
2212
+ console.error(chalk15.dim("\u23F3 queued \u2014 waiting for a submission slot"));
2069
2213
  }
2070
2214
  } else if (log.status === "running" && !printedRunning) {
2071
2215
  printedRunning = true;
2072
2216
  if (opts.json) {
2073
2217
  emit(opts.json, { event: "status", id: created.id, status: "running" });
2074
2218
  } else {
2075
- console.error(`${chalk14.cyan("\u25B6")} submitting`);
2219
+ console.error(`${chalk15.cyan("\u25B6")} submitting`);
2076
2220
  }
2077
2221
  }
2078
2222
  if (log.content) {
@@ -2114,7 +2258,7 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2114
2258
  });
2115
2259
  if (!opts.json) {
2116
2260
  const ok = final.status === "succeeded";
2117
- const mark = ok ? chalk14.green("\u2713") : chalk14.red("\u2717");
2261
+ const mark = ok ? chalk15.green("\u2713") : chalk15.red("\u2717");
2118
2262
  const duration = formatDuration(final.started_at, final.finished_at);
2119
2263
  let line = `${mark} ${final.status}`;
2120
2264
  if (duration) line += ` (${duration})`;
@@ -2126,11 +2270,11 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2126
2270
  function printError2(opts, err) {
2127
2271
  const message = err instanceof Error ? err.message : String(err);
2128
2272
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2129
- console.error(chalk14.red(`error: ${message}`));
2273
+ console.error(chalk15.red(`error: ${message}`));
2130
2274
  }
2131
2275
 
2132
2276
  // src/commands/versions.ts
2133
- import chalk15 from "chalk";
2277
+ import chalk16 from "chalk";
2134
2278
  function defaultDeps3(client) {
2135
2279
  return {
2136
2280
  listAppVersions: client.listAppVersions.bind(client),
@@ -2180,7 +2324,7 @@ async function versionsSet(buildNumber, opts, deps) {
2180
2324
  console.log(JSON.stringify(version));
2181
2325
  } else {
2182
2326
  console.log(
2183
- chalk15.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk15.dim(
2327
+ chalk16.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk16.dim(
2184
2328
  " \u2014 the next autoIncrement production build gets " + (version.build_number + 1)
2185
2329
  )
2186
2330
  );
@@ -2200,18 +2344,18 @@ function formatVersionsHuman(versions2) {
2200
2344
  const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2201
2345
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2202
2346
  const join4 = (cells) => cells.join(" ").trimEnd();
2203
- const lines = [chalk15.bold(join4(padCells(HEADERS2)))];
2347
+ const lines = [chalk16.bold(join4(padCells(HEADERS2)))];
2204
2348
  for (const row of rows) lines.push(join4(padCells(row)));
2205
2349
  return lines.join("\n") + "\n";
2206
2350
  }
2207
2351
  function printError3(opts, err) {
2208
2352
  const message = err instanceof Error ? err.message : String(err);
2209
2353
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2210
- console.error(chalk15.red(`error: ${message}`));
2354
+ console.error(chalk16.red(`error: ${message}`));
2211
2355
  }
2212
2356
 
2213
2357
  // src/commands/whoami.ts
2214
- import chalk16 from "chalk";
2358
+ import chalk17 from "chalk";
2215
2359
  function defaultDeps4(opts) {
2216
2360
  const { token, baseUrl } = resolveAuth(opts);
2217
2361
  const client = new ApiClient(baseUrl, token);
@@ -2223,6 +2367,15 @@ function effectiveRole(role, tokenRole) {
2223
2367
  if (!tokenRole) return role;
2224
2368
  return (RANK[tokenRole] ?? 3) < (RANK[role] ?? 3) ? tokenRole : role;
2225
2369
  }
2370
+ function describeExpiry(expiresAt, now = /* @__PURE__ */ new Date()) {
2371
+ if (!expiresAt) return "never";
2372
+ const when = new Date(expiresAt);
2373
+ if (Number.isNaN(when.getTime())) return expiresAt;
2374
+ const date = when.toISOString().slice(0, 10);
2375
+ const days = Math.ceil((when.getTime() - now.getTime()) / 864e5);
2376
+ if (days <= 0) return `${date} (expired)`;
2377
+ return `${date} (in ${days} day${days === 1 ? "" : "s"})`;
2378
+ }
2226
2379
  async function whoAmI(opts, deps) {
2227
2380
  const d = deps ?? defaultDeps4(opts);
2228
2381
  try {
@@ -2233,27 +2386,33 @@ async function whoAmI(opts, deps) {
2233
2386
  process.exitCode = 0;
2234
2387
  return;
2235
2388
  }
2236
- console.log(`${chalk16.bold("Logged in as")} ${me.email}`);
2389
+ console.log(`${chalk17.bold("Logged in as")} ${me.email}`);
2237
2390
  if (me.memberships.length === 1) {
2238
- console.log(`${chalk16.bold("Organization")} ${me.memberships[0].organization_name}`);
2391
+ console.log(`${chalk17.bold("Organization")} ${me.memberships[0].organization_name}`);
2239
2392
  } else if (me.memberships.length > 1) {
2240
- console.log(chalk16.bold("Organizations"));
2393
+ console.log(chalk17.bold("Organizations"));
2241
2394
  for (const m of me.memberships) {
2242
2395
  console.log(` ${m.organization_name} (${m.role})`);
2243
2396
  }
2244
2397
  }
2245
- console.log(`${chalk16.bold("Member role")} ${me.role ?? "\u2014"}`);
2246
- console.log(`${chalk16.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
2247
- console.log(`${chalk16.bold("Effective role")} ${effective ?? "\u2014"}`);
2398
+ console.log(`${chalk17.bold("Member role")} ${me.role ?? "\u2014"}`);
2399
+ console.log(`${chalk17.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
2400
+ console.log(`${chalk17.bold("Effective role")} ${effective ?? "\u2014"}`);
2401
+ if (me.token_access) {
2402
+ console.log(
2403
+ `${chalk17.bold("Access")} ${me.token_access === "ci" ? "CI only (upload builds, run tests, read results)" : "Full access"}`
2404
+ );
2405
+ console.log(`${chalk17.bold("Expires")} ${describeExpiry(me.token_expires_at)}`);
2406
+ }
2248
2407
  if (me.is_superadmin) {
2249
- console.log(chalk16.dim("Superadmin (platform-wide; does not raise the org role above)"));
2408
+ console.log(chalk17.dim("Superadmin (platform-wide; does not raise the org role above)"));
2250
2409
  }
2251
- if (me.role === "admin" && me.token_role && me.token_role !== "admin") {
2410
+ if (me.role === "admin" && me.token_role && me.token_role !== "admin" && me.token_access !== "ci") {
2252
2411
  console.log(
2253
- chalk16.yellow(
2412
+ chalk17.yellow(
2254
2413
  `
2255
- 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 ${chalk16.bold("vibeview env set")}) will fail with a permissions error.
2256
- Create an admin API token in the dashboard and use ${chalk16.bold("--token")} or ${chalk16.bold("VIBEVIEW_API_TOKEN")}.`
2414
+ 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.
2415
+ Create an admin API token in the dashboard and use ${chalk17.bold("--token")} or ${chalk17.bold("VIBEVIEW_API_TOKEN")}.`
2257
2416
  )
2258
2417
  );
2259
2418
  }
@@ -2263,7 +2422,7 @@ Create an admin API token in the dashboard and use ${chalk16.bold("--token")} or
2263
2422
  if (opts.json) {
2264
2423
  console.log(JSON.stringify({ event: "error", message }));
2265
2424
  }
2266
- console.error(chalk16.red(`error: ${message}`));
2425
+ console.error(chalk17.red(`error: ${message}`));
2267
2426
  process.exitCode = errorExitCode(err);
2268
2427
  }
2269
2428
  }
@@ -2271,7 +2430,7 @@ Create an admin API token in the dashboard and use ${chalk16.bold("--token")} or
2271
2430
  // src/commands/env.ts
2272
2431
  import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
2273
2432
  import { basename } from "path";
2274
- import chalk17 from "chalk";
2433
+ import chalk18 from "chalk";
2275
2434
  import { password } from "@inquirer/prompts";
2276
2435
 
2277
2436
  // src/lib/gitignore.ts
@@ -2327,7 +2486,7 @@ function defaultDeps5(client) {
2327
2486
  isIgnored: (path) => isGitIgnored(path),
2328
2487
  writeFile: writeSecureFile,
2329
2488
  readFile: (path) => readFileAsync(path),
2330
- warn: (message) => console.error(chalk17.yellow(`warning: ${message}`))
2489
+ warn: (message) => console.error(chalk18.yellow(`warning: ${message}`))
2331
2490
  };
2332
2491
  }
2333
2492
  async function envList(opts, deps) {
@@ -2359,7 +2518,7 @@ function formatEnvVarsHuman(variables) {
2359
2518
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2360
2519
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2361
2520
  const join4 = (cells) => cells.join(" ").trimEnd();
2362
- const lines = [chalk17.bold(join4(padCells(HEADERS3)))];
2521
+ const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
2363
2522
  for (const row of rows) lines.push(join4(padCells(row)));
2364
2523
  return lines.join("\n") + "\n";
2365
2524
  }
@@ -2409,7 +2568,7 @@ function printSetResult(opts, result) {
2409
2568
  if (opts.json) {
2410
2569
  console.log(JSON.stringify(result));
2411
2570
  } else {
2412
- console.log(chalk17.green(`\u2713 set ${result.name} (${result.visibility})`));
2571
+ console.log(chalk18.green(`\u2713 set ${result.name} (${result.visibility})`));
2413
2572
  }
2414
2573
  }
2415
2574
  async function envRm(name, opts, deps) {
@@ -2419,7 +2578,7 @@ async function envRm(name, opts, deps) {
2419
2578
  if (opts.json) {
2420
2579
  console.log(JSON.stringify({ event: "deleted", name }));
2421
2580
  } else {
2422
- console.log(chalk17.green(`\u2713 removed ${name}`));
2581
+ console.log(chalk18.green(`\u2713 removed ${name}`));
2423
2582
  }
2424
2583
  process.exitCode = 0;
2425
2584
  } catch (err) {
@@ -2451,7 +2610,7 @@ async function envPull(opts, deps) {
2451
2610
  if (opts.json) {
2452
2611
  console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
2453
2612
  } else {
2454
- console.log(chalk17.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
2613
+ console.log(chalk18.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
2455
2614
  }
2456
2615
  process.exitCode = 0;
2457
2616
  } catch (err) {
@@ -2490,7 +2649,7 @@ function escapeEnvValue(value) {
2490
2649
  function printError4(opts, err) {
2491
2650
  const message = err instanceof Error ? err.message : String(err);
2492
2651
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2493
- console.error(chalk17.red(`error: ${message}`));
2652
+ console.error(chalk18.red(`error: ${message}`));
2494
2653
  }
2495
2654
 
2496
2655
  // src/commands/credentials.ts
@@ -2502,7 +2661,7 @@ import {
2502
2661
  } from "fs/promises";
2503
2662
  import { mkdirSync } from "fs";
2504
2663
  import { basename as basename2, extname, join } from "path";
2505
- import chalk18 from "chalk";
2664
+ import chalk19 from "chalk";
2506
2665
  import { input, password as password2 } from "@inquirer/prompts";
2507
2666
  function clientFor2(opts) {
2508
2667
  const { token, baseUrl } = resolveAuth(opts);
@@ -2575,7 +2734,7 @@ function defaultDeps6(client) {
2575
2734
  mkdir: (dir) => mkdirSync(dir, { recursive: true }),
2576
2735
  writeFile: (path, data, mode) => writeFileAsync(path, data, mode !== void 0 ? { mode } : void 0),
2577
2736
  chmod: (path, mode) => chmodAsync(path, mode),
2578
- warn: (message) => console.error(chalk18.yellow(`warning: ${message}`))
2737
+ warn: (message) => console.error(chalk19.yellow(`warning: ${message}`))
2579
2738
  };
2580
2739
  }
2581
2740
  var ANDROID_KIND = "android_keystore";
@@ -2644,7 +2803,7 @@ async function credentialsAdd(opts, deps) {
2644
2803
  if (opts.json) {
2645
2804
  console.log(JSON.stringify(result));
2646
2805
  } else {
2647
- console.log(chalk18.green(`\u2713 added ${result.name} (${result.platform})`));
2806
+ console.log(chalk19.green(`\u2713 added ${result.name} (${result.platform})`));
2648
2807
  }
2649
2808
  process.exitCode = 0;
2650
2809
  } catch (err) {
@@ -2702,20 +2861,20 @@ async function credentialsGenerate(opts, deps) {
2702
2861
  }
2703
2862
  if (opts.json) {
2704
2863
  } else {
2705
- console.log(chalk18.green(`\u2713 generated ${result.name} (android)`));
2864
+ console.log(chalk19.green(`\u2713 generated ${result.name} (android)`));
2706
2865
  console.log("");
2707
- console.log(` Keystore saved to: ${chalk18.bold(savedTo)}`);
2708
- console.log(` Key alias: ${chalk18.bold(result.key_alias)}`);
2709
- console.log(` Store password: ${chalk18.bold(result.store_password)}`);
2710
- console.log(` Key password: ${chalk18.bold(result.key_password)}`);
2866
+ console.log(` Keystore saved to: ${chalk19.bold(savedTo)}`);
2867
+ console.log(` Key alias: ${chalk19.bold(result.key_alias)}`);
2868
+ console.log(` Store password: ${chalk19.bold(result.store_password)}`);
2869
+ console.log(` Key password: ${chalk19.bold(result.key_password)}`);
2711
2870
  console.log("");
2712
2871
  console.log(
2713
- chalk18.yellow(
2872
+ chalk19.yellow(
2714
2873
  " These are shown ONCE \u2014 VibeView cannot recover the keystore file or its passwords. Back them up somewhere safe."
2715
2874
  )
2716
2875
  );
2717
2876
  console.log(
2718
- chalk18.yellow(
2877
+ chalk19.yellow(
2719
2878
  " The credential is already uploaded and active for android and androidtv production builds."
2720
2879
  )
2721
2880
  );
@@ -2741,19 +2900,19 @@ async function generateAppleCredential(platform, opts, d) {
2741
2900
  if (opts.json) {
2742
2901
  console.log(JSON.stringify(result));
2743
2902
  } else {
2744
- console.log(chalk18.green(`\u2713 generated ${result.name} (${result.platform})`));
2903
+ console.log(chalk19.green(`\u2713 generated ${result.name} (${result.platform})`));
2745
2904
  console.log("");
2746
- console.log(` Scope: ${chalk18.bold(result.app_public_id ?? "org-wide")}`);
2905
+ console.log(` Scope: ${chalk19.bold(result.app_public_id ?? "org-wide")}`);
2747
2906
  const expires = formatExpires(result);
2748
2907
  if (expires) {
2749
- console.log(` Expires: ${chalk18.bold(expires)}`);
2908
+ console.log(` Expires: ${chalk19.bold(expires)}`);
2750
2909
  }
2751
2910
  console.log("");
2752
2911
  console.log(
2753
2912
  ` The certificate and provisioning profile are stored with VibeView and already active for ${result.platform} production builds.`
2754
2913
  );
2755
2914
  console.log(
2756
- ` Run ${chalk18.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
2915
+ ` Run ${chalk19.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
2757
2916
  );
2758
2917
  }
2759
2918
  process.exitCode = 0;
@@ -2819,17 +2978,17 @@ async function credentialsDownload(name, opts, deps) {
2819
2978
  await d.chmod(path, 384);
2820
2979
  }
2821
2980
  if (!opts.json) {
2822
- console.log(chalk18.green(`\u2713 downloaded ${material.name} (${material.platform})`));
2981
+ console.log(chalk19.green(`\u2713 downloaded ${material.name} (${material.platform})`));
2823
2982
  console.log("");
2824
- console.log(` Saved to: ${chalk18.bold(savedTo)}`);
2983
+ console.log(` Saved to: ${chalk19.bold(savedTo)}`);
2825
2984
  if (profileSavedTo !== null) {
2826
- console.log(` Profile: ${chalk18.bold(profileSavedTo)}`);
2985
+ console.log(` Profile: ${chalk19.bold(profileSavedTo)}`);
2827
2986
  }
2828
2987
  for (const [key, value] of Object.entries(material.passwords)) {
2829
- console.log(` ${key}: ${chalk18.bold(value)}`);
2988
+ console.log(` ${key}: ${chalk19.bold(value)}`);
2830
2989
  }
2831
2990
  console.log("");
2832
- console.log(chalk18.yellow(" This download is recorded in your organization's audit log."));
2991
+ console.log(chalk19.yellow(" This download is recorded in your organization's audit log."));
2833
2992
  }
2834
2993
  process.exitCode = 0;
2835
2994
  } catch (err) {
@@ -2860,13 +3019,13 @@ async function ascKeyAdd(opts, deps) {
2860
3019
  if (opts.json) {
2861
3020
  console.log(JSON.stringify(result));
2862
3021
  } else {
2863
- console.log(chalk18.green(`\u2713 added App Store Connect key ${result.name}`));
3022
+ console.log(chalk19.green(`\u2713 added App Store Connect key ${result.name}`));
2864
3023
  console.log("");
2865
- console.log(` Key ID: ${chalk18.bold(result.key_id)}`);
2866
- console.log(` Issuer ID: ${chalk18.bold(result.issuer_id)}`);
3024
+ console.log(` Key ID: ${chalk19.bold(result.key_id)}`);
3025
+ console.log(` Issuer ID: ${chalk19.bold(result.issuer_id)}`);
2867
3026
  console.log("");
2868
3027
  console.log(
2869
- chalk18.yellow(
3028
+ chalk19.yellow(
2870
3029
  " The .p8 is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2871
3030
  )
2872
3031
  );
@@ -2901,7 +3060,7 @@ function formatAscKeysHuman(keys) {
2901
3060
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2902
3061
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2903
3062
  const join4 = (cells) => cells.join(" ").trimEnd();
2904
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
3063
+ const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
2905
3064
  for (const row of rows) lines.push(join4(padCells(row)));
2906
3065
  return lines.join("\n") + "\n";
2907
3066
  }
@@ -2912,7 +3071,7 @@ async function ascKeyRm(name, opts, deps) {
2912
3071
  if (opts.json) {
2913
3072
  console.log(JSON.stringify({ event: "deleted", name }));
2914
3073
  } else {
2915
- console.log(chalk18.green(`\u2713 removed App Store Connect key ${name}`));
3074
+ console.log(chalk19.green(`\u2713 removed App Store Connect key ${name}`));
2916
3075
  }
2917
3076
  process.exitCode = 0;
2918
3077
  } catch (err) {
@@ -2937,12 +3096,12 @@ async function playKeyAdd(opts, deps) {
2937
3096
  if (opts.json) {
2938
3097
  console.log(JSON.stringify(result));
2939
3098
  } else {
2940
- console.log(chalk18.green(`\u2713 added Google Play key ${result.name}`));
3099
+ console.log(chalk19.green(`\u2713 added Google Play key ${result.name}`));
2941
3100
  console.log("");
2942
- console.log(` Service account: ${chalk18.bold(result.client_email)}`);
3101
+ console.log(` Service account: ${chalk19.bold(result.client_email)}`);
2943
3102
  console.log("");
2944
3103
  console.log(
2945
- chalk18.yellow(
3104
+ chalk19.yellow(
2946
3105
  " The JSON key is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2947
3106
  )
2948
3107
  );
@@ -2977,7 +3136,7 @@ function formatPlayKeysHuman(keys) {
2977
3136
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2978
3137
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2979
3138
  const join4 = (cells) => cells.join(" ").trimEnd();
2980
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
3139
+ const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
2981
3140
  for (const row of rows) lines.push(join4(padCells(row)));
2982
3141
  return lines.join("\n") + "\n";
2983
3142
  }
@@ -2988,7 +3147,7 @@ async function playKeyRm(name, opts, deps) {
2988
3147
  if (opts.json) {
2989
3148
  console.log(JSON.stringify({ event: "deleted", name }));
2990
3149
  } else {
2991
- console.log(chalk18.green(`\u2713 removed Google Play key ${name}`));
3150
+ console.log(chalk19.green(`\u2713 removed Google Play key ${name}`));
2992
3151
  }
2993
3152
  process.exitCode = 0;
2994
3153
  } catch (err) {
@@ -3057,7 +3216,7 @@ function formatCredentialsHuman(credentials2) {
3057
3216
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3058
3217
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3059
3218
  const join4 = (cells) => cells.join(" ").trimEnd();
3060
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
3219
+ const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3061
3220
  for (const row of rows) lines.push(join4(padCells(row)));
3062
3221
  return lines.join("\n") + "\n";
3063
3222
  }
@@ -3068,7 +3227,7 @@ async function credentialsRm(name, opts, deps) {
3068
3227
  if (opts.json) {
3069
3228
  console.log(JSON.stringify({ event: "deleted", name }));
3070
3229
  } else {
3071
- console.log(chalk18.green(`\u2713 removed ${name}`));
3230
+ console.log(chalk19.green(`\u2713 removed ${name}`));
3072
3231
  }
3073
3232
  process.exitCode = 0;
3074
3233
  } catch (err) {
@@ -3080,16 +3239,16 @@ function printKeystoreWriteFailure(opts, err, result) {
3080
3239
  const message = err instanceof Error ? err.message : String(err);
3081
3240
  if (opts.json) {
3082
3241
  console.log(JSON.stringify({ event: "error", message }));
3083
- console.error(chalk18.red(`error: ${message}`));
3242
+ console.error(chalk19.red(`error: ${message}`));
3084
3243
  return;
3085
3244
  }
3086
- console.error(chalk18.red(`error: ${message}`));
3245
+ console.error(chalk19.red(`error: ${message}`));
3087
3246
  console.error(
3088
- chalk18.red(
3247
+ chalk19.red(
3089
3248
  `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.`
3090
3249
  )
3091
3250
  );
3092
- console.error(chalk18.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
3251
+ console.error(chalk19.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
3093
3252
  console.error("");
3094
3253
  console.error(` Key alias: ${result.key_alias}`);
3095
3254
  console.error(` Store password: ${result.store_password}`);
@@ -3103,11 +3262,11 @@ function printKeystoreWriteFailure(opts, err, result) {
3103
3262
  function printError5(opts, err) {
3104
3263
  const message = err instanceof Error ? err.message : String(err);
3105
3264
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3106
- console.error(chalk18.red(`error: ${message}`));
3265
+ console.error(chalk19.red(`error: ${message}`));
3107
3266
  }
3108
3267
 
3109
3268
  // src/commands/webhooks.ts
3110
- import chalk19 from "chalk";
3269
+ import chalk20 from "chalk";
3111
3270
  function clientFor3(opts) {
3112
3271
  const { token, baseUrl } = resolveAuth(opts);
3113
3272
  return new ApiClient(baseUrl, token);
@@ -3138,14 +3297,14 @@ async function webhooksAdd(opts, deps) {
3138
3297
  if (opts.json) {
3139
3298
  console.log(JSON.stringify(result));
3140
3299
  } else {
3141
- console.log(chalk19.green(`\u2713 added ${result.id} ${result.url}`));
3300
+ console.log(chalk20.green(`\u2713 added ${result.id} ${result.url}`));
3142
3301
  console.log("");
3143
- console.log(` Signing secret: ${chalk19.bold(result.secret)}`);
3302
+ console.log(` Signing secret: ${chalk20.bold(result.secret)}`);
3144
3303
  if (opts.secret !== void 0) {
3145
- console.log(chalk19.yellow(" (This is the secret you provided.)"));
3304
+ console.log(chalk20.yellow(" (This is the secret you provided.)"));
3146
3305
  }
3147
3306
  console.log(
3148
- chalk19.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
3307
+ chalk20.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
3149
3308
  );
3150
3309
  }
3151
3310
  process.exitCode = 0;
@@ -3176,7 +3335,7 @@ function formatWebhooksHuman(webhooks2) {
3176
3335
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3177
3336
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3178
3337
  const join4 = (cells) => cells.join(" ").trimEnd();
3179
- const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3338
+ const lines = [chalk20.bold(join4(padCells(HEADERS3)))];
3180
3339
  for (const row of rows) lines.push(join4(padCells(row)));
3181
3340
  return lines.join("\n") + "\n";
3182
3341
  }
@@ -3202,7 +3361,7 @@ async function webhooksRm(id, opts, deps) {
3202
3361
  if (opts.json) {
3203
3362
  console.log(JSON.stringify({ event: "deleted", id }));
3204
3363
  } else {
3205
- console.log(chalk19.green(`\u2713 removed ${id}`));
3364
+ console.log(chalk20.green(`\u2713 removed ${id}`));
3206
3365
  }
3207
3366
  process.exitCode = 0;
3208
3367
  } catch (err) {
@@ -3235,13 +3394,13 @@ async function webhooksTest(id, opts, deps) {
3235
3394
  if (opts.json) console.log(JSON.stringify(ld));
3236
3395
  if (ld.status === "succeeded") {
3237
3396
  if (!opts.json) {
3238
- console.log(chalk19.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3397
+ console.log(chalk20.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3239
3398
  }
3240
3399
  process.exitCode = 0;
3241
3400
  } else {
3242
3401
  if (!opts.json) {
3243
3402
  const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
3244
- console.error(chalk19.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3403
+ console.error(chalk20.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3245
3404
  }
3246
3405
  process.exitCode = 1;
3247
3406
  }
@@ -3250,7 +3409,7 @@ async function webhooksTest(id, opts, deps) {
3250
3409
  }
3251
3410
  if (opts.json) console.log(JSON.stringify({ status: "pending" }));
3252
3411
  console.error(
3253
- chalk19.yellow(
3412
+ chalk20.yellow(
3254
3413
  `Delivery still pending after ${TEST_POLL_BUDGET_MS / 1e3}s \u2014 check \`vibeview webhooks list\` later for the result.`
3255
3414
  )
3256
3415
  );
@@ -3263,7 +3422,7 @@ async function webhooksTest(id, opts, deps) {
3263
3422
  function printError6(opts, err) {
3264
3423
  const message = err instanceof Error ? err.message : String(err);
3265
3424
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3266
- console.error(chalk19.red(`error: ${message}`));
3425
+ console.error(chalk20.red(`error: ${message}`));
3267
3426
  }
3268
3427
 
3269
3428
  // src/commands/list-devices.ts
@@ -3361,7 +3520,7 @@ import { dirname, join as join2 } from "path";
3361
3520
  import { fileURLToPath } from "url";
3362
3521
  import { spawnSync } from "child_process";
3363
3522
  import { confirm } from "@inquirer/prompts";
3364
- import chalk20 from "chalk";
3523
+ import chalk21 from "chalk";
3365
3524
  function resolveShippedSkillPath(fromDir) {
3366
3525
  for (const rel of ["..", join2("..", "..")]) {
3367
3526
  const candidate = join2(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
@@ -3389,23 +3548,23 @@ function registerMcp() {
3389
3548
  encoding: "utf8"
3390
3549
  });
3391
3550
  if (res.status === 0) {
3392
- console.log(chalk20.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
3551
+ console.log(chalk21.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
3393
3552
  return;
3394
3553
  }
3395
3554
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
3396
3555
  if (out.includes("already exists")) {
3397
- console.log(chalk20.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
3556
+ console.log(chalk21.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
3398
3557
  return;
3399
3558
  }
3400
- console.error(chalk20.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
3401
- console.error(chalk20.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
3559
+ console.error(chalk21.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
3560
+ console.error(chalk21.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
3402
3561
  }
3403
3562
  async function agentSetup(opts) {
3404
3563
  const moduleDir = dirname(fileURLToPath(import.meta.url));
3405
3564
  const shipped = resolveShippedSkillPath(moduleDir);
3406
3565
  if (!shipped) {
3407
3566
  console.error(
3408
- chalk20.red(
3567
+ chalk21.red(
3409
3568
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
3410
3569
  )
3411
3570
  );
@@ -3414,9 +3573,9 @@ async function agentSetup(opts) {
3414
3573
  }
3415
3574
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
3416
3575
  const result = installSkill(shipped, targetDir);
3417
- console.log(chalk20.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
3576
+ console.log(chalk21.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
3418
3577
  console.log(
3419
- chalk20.dim(
3578
+ chalk21.dim(
3420
3579
  opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
3421
3580
  )
3422
3581
  );
@@ -3424,7 +3583,7 @@ async function agentSetup(opts) {
3424
3583
  if (!claudeCliAvailable()) {
3425
3584
  if (opts.mcp === true) {
3426
3585
  console.error(
3427
- chalk20.yellow(
3586
+ chalk21.yellow(
3428
3587
  "\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
3429
3588
  )
3430
3589
  );
@@ -3438,7 +3597,7 @@ async function agentSetup(opts) {
3438
3597
  }
3439
3598
  console.log(
3440
3599
  `
3441
- Suggested line for your project's ${chalk20.bold("CLAUDE.md")} (or ${chalk20.bold(
3600
+ Suggested line for your project's ${chalk21.bold("CLAUDE.md")} (or ${chalk21.bold(
3442
3601
  "AGENTS.md"
3443
3602
  )} for other agents):
3444
3603
  `
@@ -3446,7 +3605,7 @@ Suggested line for your project's ${chalk20.bold("CLAUDE.md")} (or ${chalk20.bol
3446
3605
  console.log(` ${CLAUDE_MD_SUGGESTION}
3447
3606
  `);
3448
3607
  console.log(
3449
- chalk20.dim(
3608
+ chalk21.dim(
3450
3609
  "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"
3451
3610
  )
3452
3611
  );
@@ -3568,11 +3727,25 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
3568
3727
  ).option(
3569
3728
  "--device <device_id>",
3570
3729
  "Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
3730
+ ).option(
3731
+ "--platform <platform>",
3732
+ "Run the suite on another platform (ios, android, tvos, androidtv): runs your app there with the same bundle id / package"
3733
+ ).option(
3734
+ "--app <app-id>",
3735
+ "Run the suite against this app instead of its own (e.g. another platform build with a different bundle id)"
3571
3736
  ).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) => {
3572
3737
  if (opts.device && opts.deviceCategory) {
3573
3738
  process.stderr.write("error: --device cannot be combined with --device-category\n");
3574
3739
  process.exit(2);
3575
3740
  }
3741
+ try {
3742
+ parseRunPlatform(opts.platform);
3743
+ refusePlatformWithApp(opts);
3744
+ } catch (err) {
3745
+ process.stderr.write(`error: ${err.message}
3746
+ `);
3747
+ process.exit(2);
3748
+ }
3576
3749
  const metadata = resolveMetadata(opts);
3577
3750
  return runSuite(suiteId, {
3578
3751
  ...opts,
@@ -3601,6 +3774,13 @@ program.command("run-suites").description("Run all suites matching a tag filter"
3601
3774
  process.stderr.write("error: --device cannot be combined with --device-category\n");
3602
3775
  process.exit(2);
3603
3776
  }
3777
+ try {
3778
+ parseRunPlatform(opts.platform);
3779
+ } catch (err) {
3780
+ process.stderr.write(`error: ${err.message}
3781
+ `);
3782
+ process.exit(2);
3783
+ }
3604
3784
  const metadata = resolveMetadata(opts);
3605
3785
  return runSuites({
3606
3786
  ...opts,
@@ -3616,11 +3796,25 @@ program.command("run-test <test-id>").description("Run a single test case").opti
3616
3796
  ).option(
3617
3797
  "--device <device_id>",
3618
3798
  "Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
3799
+ ).option(
3800
+ "--platform <platform>",
3801
+ "Run the test on another platform (ios, android, tvos, androidtv): runs your app there with the same bundle id / package"
3802
+ ).option(
3803
+ "--app <app-id>",
3804
+ "Run the test against this app instead of its own (e.g. another platform build with a different bundle id)"
3619
3805
  ).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) => {
3620
3806
  if (opts.device && opts.deviceCategory) {
3621
3807
  process.stderr.write("error: --device cannot be combined with --device-category\n");
3622
3808
  process.exit(2);
3623
3809
  }
3810
+ try {
3811
+ parseRunPlatform(opts.platform);
3812
+ refusePlatformWithApp(opts);
3813
+ } catch (err) {
3814
+ process.stderr.write(`error: ${err.message}
3815
+ `);
3816
+ process.exit(2);
3817
+ }
3624
3818
  const metadata = resolveMetadata(opts);
3625
3819
  return runTest(testId, {
3626
3820
  ...opts,
@@ -3669,7 +3863,10 @@ program.command("dev").description(
3669
3863
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--web-url <url>", "Dashboard URL if different from the API URL (local dev)").option(
3670
3864
  "--platform <platform>",
3671
3865
  "ios, android, tvos, androidtv, or roku (first run is interactive otherwise)"
3672
- ).option("--app <app_id>", "Uploaded debug build to run (see `vibeview list-apps`)").option("--metro-port <port>", "Local Metro port (default: 8081, remembered in vibeview.json)").option(
3866
+ ).option("--app <app_id>", "Uploaded debug build to run (see `vibeview list-apps`)").option(
3867
+ "--metro-port <port>",
3868
+ "Local Metro port (default: 8081, or vibeview.json); remembered for you in .vibeview/local.json"
3869
+ ).option(
3673
3870
  "--device-category <category>",
3674
3871
  "phone or tablet (default: phone; TV platforms always use tv)",
3675
3872
  parseDeviceCategory
@@ -3831,7 +4028,7 @@ program.command("dev-reload").description(
3831
4028
  program.command("mcp").description(
3832
4029
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
3833
4030
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
3834
- const { runMcpServer } = await import("./server-IGY2VPYT.js");
4031
+ const { runMcpServer } = await import("./server-GPFOZOFL.js");
3835
4032
  await runMcpServer({ ...opts, version: pkg.version });
3836
4033
  });
3837
4034
  program.command("agent-setup").description(