vibeview 0.30.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
@@ -23,7 +23,7 @@ import {
23
23
  readDevState,
24
24
  readMetroState,
25
25
  runAgentVerb
26
- } from "./chunk-NOZGZT3G.js";
26
+ } from "./chunk-P6UIJALK.js";
27
27
  import {
28
28
  ApiClient,
29
29
  ApiStatusError,
@@ -51,7 +51,7 @@ import { createRequire } from "module";
51
51
  import { Command, Option } from "commander";
52
52
 
53
53
  // src/commands/run-suite.ts
54
- import chalk3 from "chalk";
54
+ import chalk4 from "chalk";
55
55
 
56
56
  // src/lib/poll.ts
57
57
  import chalk2 from "chalk";
@@ -145,6 +145,10 @@ function formatHuman(result, verbose) {
145
145
  lines.push(`
146
146
  ${chalk.red("Error:")} ${result.error_message}`);
147
147
  }
148
+ if (result.build_warning && result.status !== "passed") {
149
+ lines.push(`
150
+ ${chalk.yellow("Note:")} ${result.build_warning}`);
151
+ }
148
152
  lines.push("");
149
153
  return lines.join("\n");
150
154
  }
@@ -174,9 +178,16 @@ function formatJunit(result) {
174
178
  lines.push(
175
179
  ` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}" timestamp="${timestamp}">`
176
180
  );
177
- if (result.url) {
181
+ if (result.url || result.build_warning) {
178
182
  lines.push(" <properties>");
179
- 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
+ }
180
191
  lines.push(" </properties>");
181
192
  }
182
193
  for (const tr of cases) {
@@ -225,46 +236,97 @@ function formatJunitError(name, message, url) {
225
236
  return lines.join("\n");
226
237
  }
227
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
+ }
228
257
  function formatMultiSuiteJunit(result, opts = {}) {
229
- const unfinished = (status) => opts.timedOut !== void 0 && !MULTI_TERMINAL.has(status);
230
- const errored = (status) => status === "error" || unfinished(status);
231
- const tests = result.suite_runs.reduce((sum, sr) => sum + sr.passed + sr.failed, 0);
232
- const failures = result.suite_runs.reduce((sum, sr) => sum + sr.failed, 0);
233
- const noSuites = opts.timedOut !== void 0 && result.suite_runs.length === 0;
234
- 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 ?? "");
235
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);
236
306
  const lines = [];
237
307
  lines.push('<?xml version="1.0" encoding="UTF-8"?>');
238
308
  lines.push(
239
- `<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")}">`
240
310
  );
241
- for (const sr of result.suite_runs) {
242
- const suiteName = escapeXml(sr.suite_name || sr.suite_id);
243
- const suiteTests = sr.passed + sr.failed;
244
- const suiteFails = sr.failed;
245
- const suiteErrors = errored(sr.status) ? 1 : 0;
311
+ for (const s of suites) {
312
+ const suiteName = escapeXml(s.suiteName);
246
313
  lines.push(
247
- ` <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}">`
248
315
  );
249
- if (sr.url) {
316
+ if (s.sr.url) {
250
317
  lines.push(" <properties>");
251
- lines.push(` <property name="vibeview.run_url" value="${escapeXml(sr.url)}" />`);
318
+ lines.push(` <property name="vibeview.run_url" value="${escapeXml(s.sr.url)}" />`);
252
319
  lines.push(" </properties>");
253
320
  }
254
- if (unfinished(sr.status)) {
255
- const msg = escapeXml(`${opts.timedOut} (suite was ${sr.status})`);
256
- lines.push(` <error message="${msg}" type="InfraError" />`);
257
- } else if (suiteErrors > 0) {
258
- lines.push(` <error message="Suite errored" type="InfraError" />`);
259
- }
321
+ for (const c of s.cases) lines.push(...caseLines(c, suiteName));
260
322
  lines.push(" </testsuite>");
261
323
  }
262
324
  if (noSuites) {
263
- const msg = escapeXml(opts.timedOut);
325
+ const name = "vibeview run-suites";
264
326
  lines.push(
265
- ` <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}">`
266
328
  );
267
- lines.push(` <error message="${msg}" type="InfraError" />`);
329
+ lines.push(...caseLines({ name, time: "0", verdict: "error", message: opts.timedOut }, name));
268
330
  lines.push(" </testsuite>");
269
331
  }
270
332
  lines.push("</testsuites>");
@@ -572,6 +634,7 @@ function mapExitCode(status) {
572
634
  }
573
635
 
574
636
  // src/lib/run-target.ts
637
+ import chalk3 from "chalk";
575
638
  var RUN_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
576
639
  function parseRunPlatform(value) {
577
640
  if (value === void 0) return void 0;
@@ -580,6 +643,18 @@ function parseRunPlatform(value) {
580
643
  }
581
644
  return value;
582
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
+ }
583
658
  function describeRunTarget(response, options) {
584
659
  const asked = Boolean(options.platform || options.app);
585
660
  if (!response.app_id) {
@@ -668,12 +743,13 @@ ${signal} received.
668
743
  const target = describeRunTarget(response, options);
669
744
  spinner.stop();
670
745
  process.stderr.write(
671
- `${chalk3.dim("Suite:")} ${suiteId}
672
- ` + (target ? `${chalk3.dim("App:")} ${target}
673
- ` : "") + `${chalk3.dim("Run:")} ${response.run_id}
674
- ` + (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}
675
750
  ` : "")
676
751
  );
752
+ printBuildWarning(response.build_warning);
677
753
  spinner.start("Waiting for device...");
678
754
  const timeoutMs = (options.timeout ?? 300) * 1e3;
679
755
  const result = await pollUntilComplete(
@@ -691,7 +767,10 @@ ${signal} received.
691
767
  process.stdin.setRawMode(false);
692
768
  process.stdin.pause();
693
769
  }
694
- outputResult(result, options);
770
+ outputResult(
771
+ response.build_warning ? { ...result, build_warning: response.build_warning } : result,
772
+ options
773
+ );
695
774
  process.exit(mapExitCode(result.status));
696
775
  } catch (err) {
697
776
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
@@ -717,7 +796,7 @@ ${signal} received.
717
796
  }
718
797
 
719
798
  // src/commands/run-suites.ts
720
- import chalk4 from "chalk";
799
+ import chalk5 from "chalk";
721
800
  var AGGREGATE_TIMEOUT_CAP_S = 7200;
722
801
  async function runSuites(opts) {
723
802
  if (!opts.tag || opts.tag.length === 0) {
@@ -792,13 +871,13 @@ ${signal} received.
792
871
  const noMatch = `No suites match tags: ${opts.tag.join(", ")}${suffix}`;
793
872
  if (opts.output === "json") printJsonError(noMatch);
794
873
  if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", noMatch));
795
- process.stderr.write(chalk4.yellow(`${noMatch}
874
+ process.stderr.write(chalk5.yellow(`${noMatch}
796
875
  `));
797
876
  process.exit(EXIT_INFRA_ERROR);
798
877
  }
799
878
  if (opts.output === "json") printJsonError(err);
800
879
  if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", msg));
801
- process.stderr.write(chalk4.red(`Failed to start: ${msg}
880
+ process.stderr.write(chalk5.red(`Failed to start: ${msg}
802
881
  `));
803
882
  process.exit(EXIT_INFRA_ERROR);
804
883
  }
@@ -807,16 +886,17 @@ ${signal} received.
807
886
  const matchedNames = start.matched_suites?.map((s) => s.name) ?? [];
808
887
  const totalMatched = start.matched_suite_ids.length;
809
888
  process.stderr.write(
810
- `${chalk4.dim("Aggregate:")} ${start.aggregate_id}
811
- ${chalk4.dim("Tags:")} ${opts.tag.join(", ")} ${chalk4.dim("(" + (opts.match ?? "any") + ")")}
812
- ${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(", ") : ""}
813
892
  `
814
893
  );
894
+ for (const s of start.matched_suites ?? []) printBuildWarning(s.build_warning, s.name);
815
895
  const excluded = start.excluded_suites ?? [];
816
896
  if (excluded.length) {
817
897
  const names = excluded.map((s) => s.name).join(", ");
818
898
  process.stderr.write(
819
- chalk4.dim(`Excluded ${excluded.length} suite(s) (platform mismatch): ${names}
899
+ chalk5.dim(`Excluded ${excluded.length} suite(s) (platform mismatch): ${names}
820
900
  `)
821
901
  );
822
902
  }
@@ -830,20 +910,28 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
830
910
  const suiteStateBefore = /* @__PURE__ */ new Map();
831
911
  const suiteStartedPrinted = /* @__PURE__ */ new Set();
832
912
  let lastQueueReason = null;
913
+ let pending = [];
833
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;
834
920
  spinner.stop();
835
- process.stderr.write(line + "\n");
836
- spinner.start();
921
+ for (const line of pending) process.stderr.write(line + "\n");
922
+ pending = [];
923
+ if (nextText !== null) spinner.start(nextText);
837
924
  };
838
925
  const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
839
926
  let last = null;
840
927
  const abandon = async (reason) => {
928
+ flush(null);
841
929
  spinner.stop();
842
930
  if (process.stdin.isTTY) {
843
931
  process.stdin.setRawMode(false);
844
932
  process.stdin.pause();
845
933
  }
846
- process.stderr.write(chalk4.red(`${reason}
934
+ process.stderr.write(chalk5.red(`${reason}
847
935
  `));
848
936
  process.stderr.write("Cancelling multi-suite run...\n");
849
937
  try {
@@ -851,7 +939,7 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
851
939
  process.stderr.write("Cancellation requested.\n");
852
940
  } catch (err) {
853
941
  const m = err instanceof Error ? err.message : String(err);
854
- process.stderr.write(chalk4.yellow(`Could not cancel the run: ${m}
942
+ process.stderr.write(chalk5.yellow(`Could not cancel the run: ${m}
855
943
  `));
856
944
  }
857
945
  if (opts.output === "json") {
@@ -879,6 +967,7 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
879
967
  return abandon(`Could not read the run's status: ${m}`);
880
968
  }
881
969
  let doneCount = 0;
970
+ let nextText;
882
971
  let queuedChild;
883
972
  for (const sr of last.suite_runs) {
884
973
  const cur = sr.status;
@@ -888,17 +977,17 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
888
977
  }
889
978
  if (cur === "running" && !suiteStartedPrinted.has(sr.run_id)) {
890
979
  suiteStartedPrinted.add(sr.run_id);
891
- print(`${chalk4.cyan("\u25B6")} ${chalk4.bold(sr.suite_name)} started`);
980
+ print(`${chalk5.cyan("\u25B6")} ${chalk5.bold(sr.suite_name)} started`);
892
981
  }
893
982
  const wasTerminal = prev !== void 0 && TERMINAL.has(prev);
894
983
  if (TERMINAL.has(cur) && !wasTerminal) {
895
- const mark = cur === "passed" ? chalk4.green("\u2713") : cur === "cancelled" ? chalk4.yellow("\u26A0") : chalk4.red("\u2717");
896
- 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) : "";
897
986
  print(
898
- `${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}`
899
988
  );
900
989
  if (sr.error_message) {
901
- print(` ${chalk4.red(sr.error_message)}`);
990
+ print(` ${chalk5.red(sr.error_message)}`);
902
991
  }
903
992
  }
904
993
  suiteStateBefore.set(sr.run_id, cur);
@@ -911,16 +1000,20 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
911
1000
  const why = queueReasonCopy(q.reason);
912
1001
  if (why && why !== lastQueueReason) {
913
1002
  print(
914
- `${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}`
915
1004
  );
916
1005
  }
917
1006
  lastQueueReason = why;
918
- 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)`;
919
1008
  } else {
920
1009
  lastQueueReason = null;
921
- 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;
922
1015
  }
923
- if (TERMINAL.has(last.status)) break;
1016
+ flush(nextText);
924
1017
  await new Promise((r) => setTimeout(r, 2e3));
925
1018
  }
926
1019
  if (!last || !TERMINAL.has(last.status)) {
@@ -934,7 +1027,9 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
934
1027
  process.stdin.pause();
935
1028
  }
936
1029
  if (opts.output === "json") {
937
- 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");
938
1033
  } else if (opts.output === "junit") {
939
1034
  process.stdout.write(formatMultiSuiteJunit(last) + "\n");
940
1035
  } else {
@@ -942,25 +1037,25 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
942
1037
  Aggregate: ${last.status}
943
1038
  `);
944
1039
  for (const sr of last.suite_runs) {
945
- const mark = sr.status === "passed" ? chalk4.green("\u2713") : chalk4.red("\u2717");
946
- 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}`) : "";
947
1042
  process.stdout.write(
948
1043
  ` ${mark} ${sr.suite_name}: ${sr.passed} passed, ${sr.failed} failed${link}
949
1044
  `
950
1045
  );
951
1046
  if (sr.error_message) {
952
- process.stdout.write(` ${chalk4.red(sr.error_message)}
1047
+ process.stdout.write(` ${chalk5.red(sr.error_message)}
953
1048
  `);
954
1049
  }
955
1050
  for (const cr of sr.case_runs ?? []) {
956
- const caseMark = cr.status === "passed" ? chalk4.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk4.yellow("-") : chalk4.red("\u2717");
957
- 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)`) : "";
958
1053
  process.stdout.write(
959
- ` ${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)}
960
1055
  `
961
1056
  );
962
1057
  if (cr.error_message && cr.status !== "passed") {
963
- process.stdout.write(` ${chalk4.red(cr.error_message)}
1058
+ process.stdout.write(` ${chalk5.red(cr.error_message)}
964
1059
  `);
965
1060
  }
966
1061
  }
@@ -972,7 +1067,7 @@ Aggregate: ${last.status}
972
1067
  }
973
1068
 
974
1069
  // src/commands/run-test.ts
975
- import chalk5 from "chalk";
1070
+ import chalk6 from "chalk";
976
1071
  async function runTest(testId, options) {
977
1072
  const { token, baseUrl } = resolveAuth(options);
978
1073
  const client = new ApiClient(baseUrl, token);
@@ -1046,13 +1141,14 @@ ${signal} received.
1046
1141
  const target = describeRunTarget(response, options);
1047
1142
  spinner.stop();
1048
1143
  process.stderr.write(
1049
- `${chalk5.dim("Session:")} ${sessionId || "unknown"}
1050
- ${chalk5.dim("Device:")} ${response.device_name || "allocating..."}
1051
- ${chalk5.dim("Test:")} ${testId}
1052
- ` + (target ? `${chalk5.dim("App:")} ${target}
1053
- ` : "") + `${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}
1054
1149
  `
1055
1150
  );
1151
+ printBuildWarning(response.build_warning);
1056
1152
  spinner.start("Waiting for device...");
1057
1153
  const timeoutMs = (options.timeout ?? 300) * 1e3;
1058
1154
  const result = await pollUntilComplete(
@@ -1070,7 +1166,10 @@ ${chalk5.dim("Test:")} ${testId}
1070
1166
  process.stdin.setRawMode(false);
1071
1167
  process.stdin.pause();
1072
1168
  }
1073
- outputResult(result, options);
1169
+ outputResult(
1170
+ response.build_warning ? { ...result, build_warning: response.build_warning } : result,
1171
+ options
1172
+ );
1074
1173
  process.exit(mapExitCode(result.status));
1075
1174
  } catch (err) {
1076
1175
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
@@ -1096,7 +1195,7 @@ ${chalk5.dim("Test:")} ${testId}
1096
1195
  }
1097
1196
 
1098
1197
  // src/commands/list-suites.ts
1099
- import chalk6 from "chalk";
1198
+ import chalk7 from "chalk";
1100
1199
  async function listSuites(options) {
1101
1200
  const { token, baseUrl } = resolveAuth(options);
1102
1201
  const client = new ApiClient(baseUrl, token);
@@ -1118,21 +1217,21 @@ async function listSuites(options) {
1118
1217
  console.log("No test suites found.");
1119
1218
  return;
1120
1219
  }
1121
- console.log(chalk6.bold(`
1220
+ console.log(chalk7.bold(`
1122
1221
  ${suites.length} suite(s)
1123
1222
  `));
1124
1223
  for (const s of suites) {
1125
- const platform = s.platform ? chalk6.dim(` [${s.platform}]`) : "";
1126
- 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(", ")}]`) : "";
1127
1226
  console.log(
1128
- ` ${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"})`
1129
1228
  );
1130
1229
  }
1131
1230
  console.log();
1132
1231
  }
1133
1232
 
1134
1233
  // src/commands/list-apps.ts
1135
- import chalk7 from "chalk";
1234
+ import chalk8 from "chalk";
1136
1235
  async function listApps(options) {
1137
1236
  const { token, baseUrl } = resolveAuth(options);
1138
1237
  const client = new ApiClient(baseUrl, token);
@@ -1151,20 +1250,20 @@ async function listApps(options) {
1151
1250
  console.log("No apps found.");
1152
1251
  return;
1153
1252
  }
1154
- console.log(chalk7.bold(`
1253
+ console.log(chalk8.bold(`
1155
1254
  ${apps.length} app(s)
1156
1255
  `));
1157
1256
  for (const app of apps) {
1158
- const platform = chalk7.dim(` [${app.platform}]`);
1159
- const pkg2 = chalk7.dim(app.package_name);
1160
- console.log(` ${chalk7.cyan(app.public_id)} ${app.name}${platform} ${pkg2}`);
1161
- 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}`));
1162
1261
  }
1163
1262
  console.log();
1164
1263
  }
1165
1264
 
1166
1265
  // src/commands/stop.ts
1167
- import chalk8 from "chalk";
1266
+ import chalk9 from "chalk";
1168
1267
  async function stopCommand(sessionId, opts, deps) {
1169
1268
  const id = sessionId?.trim();
1170
1269
  if (!id) throw new UsageError("a session id is required: vibeview stop <session-id>");
@@ -1175,7 +1274,7 @@ async function stopCommand(sessionId, opts, deps) {
1175
1274
  } catch (err) {
1176
1275
  const message = err instanceof Error ? err.message : String(err);
1177
1276
  if (opts.json) console.log(JSON.stringify({ stopped: false, session_id: id, message }));
1178
- console.error(chalk8.red(`error: ${message}`));
1277
+ console.error(chalk9.red(`error: ${message}`));
1179
1278
  process.exitCode = errorExitCode(err);
1180
1279
  return;
1181
1280
  }
@@ -1208,7 +1307,7 @@ async function stopCommand(sessionId, opts, deps) {
1208
1307
  }
1209
1308
 
1210
1309
  // src/commands/create-test.ts
1211
- import chalk9 from "chalk";
1310
+ import chalk10 from "chalk";
1212
1311
  import { existsSync, readFileSync } from "fs";
1213
1312
  async function readStdin() {
1214
1313
  return new Promise((resolve, reject) => {
@@ -1249,6 +1348,18 @@ async function resolveSteps(options) {
1249
1348
  }
1250
1349
  throw new Error("No steps provided. Use --steps, --steps-file, or pipe JSON to stdin.");
1251
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
+ }
1252
1363
  async function resolveAppId(client, appRef, platform) {
1253
1364
  if (appRef.includes(".")) {
1254
1365
  if (!platform) {
@@ -1270,6 +1381,7 @@ async function createTest(options) {
1270
1381
  const client = new ApiClient(baseUrl, token);
1271
1382
  try {
1272
1383
  const steps = await resolveSteps(options);
1384
+ assertHasSteps(steps);
1273
1385
  let appPublicId;
1274
1386
  if (options.app) {
1275
1387
  appPublicId = await resolveAppId(client, options.app, options.platform);
@@ -1303,20 +1415,20 @@ async function createTest(options) {
1303
1415
  );
1304
1416
  } else {
1305
1417
  console.log(`
1306
- ${chalk9.cyan(result.public_id)} ${result.name}`);
1418
+ ${chalk10.cyan(result.public_id)} ${result.name}`);
1307
1419
  if (result.app_name) {
1308
- console.log(` ${chalk9.dim(`App: ${result.app_name}`)}`);
1420
+ console.log(` ${chalk10.dim(`App: ${result.app_name}`)}`);
1309
1421
  }
1310
1422
  if (result.platform) {
1311
- console.log(` ${chalk9.dim(`Platform: ${result.platform}`)}`);
1423
+ console.log(` ${chalk10.dim(`Platform: ${result.platform}`)}`);
1312
1424
  }
1313
- console.log(` ${chalk9.dim(`Steps: ${steps.length}`)}`);
1314
- 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
1315
1427
  `));
1316
1428
  }
1317
1429
  } catch (err) {
1318
1430
  if (options.output === "json") printJsonError(err);
1319
- 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"));
1320
1432
  process.exit(
1321
1433
  err instanceof ApiStatusError && err.status === 409 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR
1322
1434
  );
@@ -1400,7 +1512,7 @@ async function logout() {
1400
1512
  }
1401
1513
 
1402
1514
  // src/commands/dev-status.ts
1403
- import chalk10 from "chalk";
1515
+ import chalk11 from "chalk";
1404
1516
  function buildLine(info) {
1405
1517
  if (!info || typeof info.app_build_id !== "number") return null;
1406
1518
  const version = info.app_version ? ` version ${info.app_version}${info.app_build_number ? ` (${info.app_build_number})` : ""}` : "";
@@ -1456,7 +1568,7 @@ async function devStatus(opts) {
1456
1568
  console.error(` url: ${state.page_url}`);
1457
1569
  console.error(` started: ${state.started_at}`);
1458
1570
  }
1459
- 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)`));
1460
1572
  process.exit(0);
1461
1573
  }
1462
1574
 
@@ -1492,12 +1604,12 @@ async function devStop(opts) {
1492
1604
  }
1493
1605
 
1494
1606
  // src/commands/dev-reload.ts
1495
- import chalk11 from "chalk";
1607
+ import chalk12 from "chalk";
1496
1608
  function fail(opts, code, message, exit) {
1497
1609
  if (opts.json) {
1498
1610
  console.log(JSON.stringify({ reloaded: false, code, message }));
1499
1611
  } else {
1500
- console.error(chalk11.red(`error: ${message}`));
1612
+ console.error(chalk12.red(`error: ${message}`));
1501
1613
  }
1502
1614
  process.exit(exit);
1503
1615
  }
@@ -1550,7 +1662,7 @@ async function devReload(opts) {
1550
1662
  api,
1551
1663
  // Build output and the "reloaded in" line are human output: stderr, so
1552
1664
  // stdout stays reserved for the --json result.
1553
- log: (line) => console.error(chalk11.dim(line))
1665
+ log: (line) => console.error(chalk12.dim(line))
1554
1666
  });
1555
1667
  const started = Date.now();
1556
1668
  try {
@@ -1571,10 +1683,10 @@ async function devReload(opts) {
1571
1683
  }
1572
1684
 
1573
1685
  // src/commands/builds.ts
1574
- import chalk13 from "chalk";
1686
+ import chalk14 from "chalk";
1575
1687
 
1576
1688
  // src/lib/formatters/builds.ts
1577
- import chalk12 from "chalk";
1689
+ import chalk13 from "chalk";
1578
1690
  var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
1579
1691
  var UNBILLED_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
1580
1692
  function formatCostCell(costMicro, status) {
@@ -1582,13 +1694,13 @@ function formatCostCell(costMicro, status) {
1582
1694
  return `$${(Math.max(costMicro, 0) / 1e6).toFixed(2)}`;
1583
1695
  }
1584
1696
  var STATUS_COLOR = {
1585
- succeeded: chalk12.green,
1586
- failed: chalk12.red,
1587
- timed_out: chalk12.red,
1588
- infra_failed: chalk12.red,
1589
- canceled: chalk12.dim,
1590
- running: chalk12.yellow,
1591
- 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
1592
1704
  };
1593
1705
  function formatBuildsHuman(builds2) {
1594
1706
  if (builds2.length === 0) {
@@ -1605,7 +1717,7 @@ function formatBuildsHuman(builds2) {
1605
1717
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1606
1718
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1607
1719
  const join4 = (cells) => cells.join(" ").trimEnd();
1608
- const lines = [chalk12.bold(join4(padCells(HEADERS)))];
1720
+ const lines = [chalk13.bold(join4(padCells(HEADERS)))];
1609
1721
  for (const row of rows) {
1610
1722
  const padded = padCells(row);
1611
1723
  const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
@@ -1664,7 +1776,7 @@ async function buildsLogs(id, opts, deps) {
1664
1776
  console.log(JSON.stringify(logs));
1665
1777
  } else {
1666
1778
  if (logs.content) process.stdout.write(logs.content);
1667
- console.error(chalk13.dim(`status: ${logs.status}`));
1779
+ console.error(chalk14.dim(`status: ${logs.status}`));
1668
1780
  }
1669
1781
  process.exitCode = 0;
1670
1782
  return;
@@ -1761,7 +1873,7 @@ async function buildsDownload(id, opts, deps) {
1761
1873
  if (opts.json) {
1762
1874
  console.log(JSON.stringify(result));
1763
1875
  } else {
1764
- console.log(chalk13.green(`\u2713 saved ${result.path}`));
1876
+ console.log(chalk14.green(`\u2713 saved ${result.path}`));
1765
1877
  }
1766
1878
  process.exitCode = 0;
1767
1879
  } catch (err) {
@@ -1778,7 +1890,7 @@ async function buildsCancel(id, opts, deps) {
1778
1890
  if (opts.json) {
1779
1891
  console.log(JSON.stringify(build));
1780
1892
  } else {
1781
- console.log(`${chalk13.cyan(build.id)} ${build.status}`);
1893
+ console.log(`${chalk14.cyan(build.id)} ${build.status}`);
1782
1894
  }
1783
1895
  process.exitCode = 0;
1784
1896
  } catch (err) {
@@ -1795,7 +1907,7 @@ async function buildsDeleteArtifact(id, opts, deps) {
1795
1907
  if (opts.json) {
1796
1908
  console.log(JSON.stringify(result));
1797
1909
  } else {
1798
- console.log(chalk13.green(`\u2713 deleted artifact for ${id}`));
1910
+ console.log(chalk14.green(`\u2713 deleted artifact for ${id}`));
1799
1911
  }
1800
1912
  process.exitCode = 0;
1801
1913
  } catch (err) {
@@ -1806,11 +1918,11 @@ async function buildsDeleteArtifact(id, opts, deps) {
1806
1918
  function printError(opts, err) {
1807
1919
  const message = err instanceof Error ? err.message : String(err);
1808
1920
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1809
- console.error(chalk13.red(`error: ${message}`));
1921
+ console.error(chalk14.red(`error: ${message}`));
1810
1922
  }
1811
1923
 
1812
1924
  // src/commands/submit.ts
1813
- import chalk14 from "chalk";
1925
+ import chalk15 from "chalk";
1814
1926
  function defaultDeps2(client) {
1815
1927
  return {
1816
1928
  createSubmission: client.createSubmission.bind(client),
@@ -1834,7 +1946,7 @@ function submitConfigFor(cwd, platform) {
1834
1946
  cfg = loadDevConfig(cwd);
1835
1947
  } catch (err) {
1836
1948
  console.error(
1837
- chalk14.yellow(
1949
+ chalk15.yellow(
1838
1950
  `warning: vibeview.json could not be read \u2014 ignoring submit defaults (${err instanceof Error ? err.message : String(err)})`
1839
1951
  )
1840
1952
  );
@@ -1982,12 +2094,12 @@ async function submitCommand(opts, deps) {
1982
2094
  return;
1983
2095
  }
1984
2096
  if (!opts.json) {
1985
- console.error(` ${chalk14.cyan(sub.id)} ${chalk14.dim(`[${sub.platform}]`)}`);
2097
+ console.error(` ${chalk15.cyan(sub.id)} ${chalk15.dim(`[${sub.platform}]`)}`);
1986
2098
  }
1987
2099
  const final = await followSubmission(sub, opts, d);
1988
2100
  if (!opts.json && final.status === "succeeded" && (final.release_status ?? releaseStatus) === "draft") {
1989
2101
  console.error(
1990
- 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")
1991
2103
  );
1992
2104
  }
1993
2105
  process.exitCode = final.status === "succeeded" ? 0 : 1;
@@ -2018,11 +2130,11 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
2018
2130
  });
2019
2131
  emit(opts.json, { event: "preflight", ...result });
2020
2132
  if (!opts.json) {
2021
- const mark = result.ready ? chalk14.green("\u2713") : chalk14.red("\u2717");
2133
+ const mark = result.ready ? chalk15.green("\u2713") : chalk15.red("\u2717");
2022
2134
  console.error(`${mark} ${result.message}`);
2023
2135
  if (result.ready) {
2024
2136
  console.error(
2025
- chalk14.dim(
2137
+ chalk15.dim(
2026
2138
  ` next: vibeview build --cloud --production --platform ${platform}, then vibeview submit --platform ${platform} --latest`
2027
2139
  )
2028
2140
  );
@@ -2056,7 +2168,7 @@ async function linkedIdentifier(platform, opts, deps) {
2056
2168
  }
2057
2169
  if (!app.package_name) return void 0;
2058
2170
  if (!opts.json) {
2059
- 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)`));
2060
2172
  }
2061
2173
  return app.package_name;
2062
2174
  }
@@ -2097,14 +2209,14 @@ async function followSubmission(created, opts, deps) {
2097
2209
  if (opts.json) {
2098
2210
  emit(opts.json, { event: "status", id: created.id, status: "queued" });
2099
2211
  } else {
2100
- 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"));
2101
2213
  }
2102
2214
  } else if (log.status === "running" && !printedRunning) {
2103
2215
  printedRunning = true;
2104
2216
  if (opts.json) {
2105
2217
  emit(opts.json, { event: "status", id: created.id, status: "running" });
2106
2218
  } else {
2107
- console.error(`${chalk14.cyan("\u25B6")} submitting`);
2219
+ console.error(`${chalk15.cyan("\u25B6")} submitting`);
2108
2220
  }
2109
2221
  }
2110
2222
  if (log.content) {
@@ -2146,7 +2258,7 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2146
2258
  });
2147
2259
  if (!opts.json) {
2148
2260
  const ok = final.status === "succeeded";
2149
- const mark = ok ? chalk14.green("\u2713") : chalk14.red("\u2717");
2261
+ const mark = ok ? chalk15.green("\u2713") : chalk15.red("\u2717");
2150
2262
  const duration = formatDuration(final.started_at, final.finished_at);
2151
2263
  let line = `${mark} ${final.status}`;
2152
2264
  if (duration) line += ` (${duration})`;
@@ -2158,11 +2270,11 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2158
2270
  function printError2(opts, err) {
2159
2271
  const message = err instanceof Error ? err.message : String(err);
2160
2272
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2161
- console.error(chalk14.red(`error: ${message}`));
2273
+ console.error(chalk15.red(`error: ${message}`));
2162
2274
  }
2163
2275
 
2164
2276
  // src/commands/versions.ts
2165
- import chalk15 from "chalk";
2277
+ import chalk16 from "chalk";
2166
2278
  function defaultDeps3(client) {
2167
2279
  return {
2168
2280
  listAppVersions: client.listAppVersions.bind(client),
@@ -2212,7 +2324,7 @@ async function versionsSet(buildNumber, opts, deps) {
2212
2324
  console.log(JSON.stringify(version));
2213
2325
  } else {
2214
2326
  console.log(
2215
- 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(
2216
2328
  " \u2014 the next autoIncrement production build gets " + (version.build_number + 1)
2217
2329
  )
2218
2330
  );
@@ -2232,18 +2344,18 @@ function formatVersionsHuman(versions2) {
2232
2344
  const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2233
2345
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2234
2346
  const join4 = (cells) => cells.join(" ").trimEnd();
2235
- const lines = [chalk15.bold(join4(padCells(HEADERS2)))];
2347
+ const lines = [chalk16.bold(join4(padCells(HEADERS2)))];
2236
2348
  for (const row of rows) lines.push(join4(padCells(row)));
2237
2349
  return lines.join("\n") + "\n";
2238
2350
  }
2239
2351
  function printError3(opts, err) {
2240
2352
  const message = err instanceof Error ? err.message : String(err);
2241
2353
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2242
- console.error(chalk15.red(`error: ${message}`));
2354
+ console.error(chalk16.red(`error: ${message}`));
2243
2355
  }
2244
2356
 
2245
2357
  // src/commands/whoami.ts
2246
- import chalk16 from "chalk";
2358
+ import chalk17 from "chalk";
2247
2359
  function defaultDeps4(opts) {
2248
2360
  const { token, baseUrl } = resolveAuth(opts);
2249
2361
  const client = new ApiClient(baseUrl, token);
@@ -2255,6 +2367,15 @@ function effectiveRole(role, tokenRole) {
2255
2367
  if (!tokenRole) return role;
2256
2368
  return (RANK[tokenRole] ?? 3) < (RANK[role] ?? 3) ? tokenRole : role;
2257
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
+ }
2258
2379
  async function whoAmI(opts, deps) {
2259
2380
  const d = deps ?? defaultDeps4(opts);
2260
2381
  try {
@@ -2265,27 +2386,33 @@ async function whoAmI(opts, deps) {
2265
2386
  process.exitCode = 0;
2266
2387
  return;
2267
2388
  }
2268
- console.log(`${chalk16.bold("Logged in as")} ${me.email}`);
2389
+ console.log(`${chalk17.bold("Logged in as")} ${me.email}`);
2269
2390
  if (me.memberships.length === 1) {
2270
- console.log(`${chalk16.bold("Organization")} ${me.memberships[0].organization_name}`);
2391
+ console.log(`${chalk17.bold("Organization")} ${me.memberships[0].organization_name}`);
2271
2392
  } else if (me.memberships.length > 1) {
2272
- console.log(chalk16.bold("Organizations"));
2393
+ console.log(chalk17.bold("Organizations"));
2273
2394
  for (const m of me.memberships) {
2274
2395
  console.log(` ${m.organization_name} (${m.role})`);
2275
2396
  }
2276
2397
  }
2277
- console.log(`${chalk16.bold("Member role")} ${me.role ?? "\u2014"}`);
2278
- console.log(`${chalk16.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
2279
- 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
+ }
2280
2407
  if (me.is_superadmin) {
2281
- 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)"));
2282
2409
  }
2283
- 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") {
2284
2411
  console.log(
2285
- chalk16.yellow(
2412
+ chalk17.yellow(
2286
2413
  `
2287
- 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.
2288
- 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")}.`
2289
2416
  )
2290
2417
  );
2291
2418
  }
@@ -2295,7 +2422,7 @@ Create an admin API token in the dashboard and use ${chalk16.bold("--token")} or
2295
2422
  if (opts.json) {
2296
2423
  console.log(JSON.stringify({ event: "error", message }));
2297
2424
  }
2298
- console.error(chalk16.red(`error: ${message}`));
2425
+ console.error(chalk17.red(`error: ${message}`));
2299
2426
  process.exitCode = errorExitCode(err);
2300
2427
  }
2301
2428
  }
@@ -2303,7 +2430,7 @@ Create an admin API token in the dashboard and use ${chalk16.bold("--token")} or
2303
2430
  // src/commands/env.ts
2304
2431
  import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
2305
2432
  import { basename } from "path";
2306
- import chalk17 from "chalk";
2433
+ import chalk18 from "chalk";
2307
2434
  import { password } from "@inquirer/prompts";
2308
2435
 
2309
2436
  // src/lib/gitignore.ts
@@ -2359,7 +2486,7 @@ function defaultDeps5(client) {
2359
2486
  isIgnored: (path) => isGitIgnored(path),
2360
2487
  writeFile: writeSecureFile,
2361
2488
  readFile: (path) => readFileAsync(path),
2362
- warn: (message) => console.error(chalk17.yellow(`warning: ${message}`))
2489
+ warn: (message) => console.error(chalk18.yellow(`warning: ${message}`))
2363
2490
  };
2364
2491
  }
2365
2492
  async function envList(opts, deps) {
@@ -2391,7 +2518,7 @@ function formatEnvVarsHuman(variables) {
2391
2518
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2392
2519
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2393
2520
  const join4 = (cells) => cells.join(" ").trimEnd();
2394
- const lines = [chalk17.bold(join4(padCells(HEADERS3)))];
2521
+ const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
2395
2522
  for (const row of rows) lines.push(join4(padCells(row)));
2396
2523
  return lines.join("\n") + "\n";
2397
2524
  }
@@ -2441,7 +2568,7 @@ function printSetResult(opts, result) {
2441
2568
  if (opts.json) {
2442
2569
  console.log(JSON.stringify(result));
2443
2570
  } else {
2444
- console.log(chalk17.green(`\u2713 set ${result.name} (${result.visibility})`));
2571
+ console.log(chalk18.green(`\u2713 set ${result.name} (${result.visibility})`));
2445
2572
  }
2446
2573
  }
2447
2574
  async function envRm(name, opts, deps) {
@@ -2451,7 +2578,7 @@ async function envRm(name, opts, deps) {
2451
2578
  if (opts.json) {
2452
2579
  console.log(JSON.stringify({ event: "deleted", name }));
2453
2580
  } else {
2454
- console.log(chalk17.green(`\u2713 removed ${name}`));
2581
+ console.log(chalk18.green(`\u2713 removed ${name}`));
2455
2582
  }
2456
2583
  process.exitCode = 0;
2457
2584
  } catch (err) {
@@ -2483,7 +2610,7 @@ async function envPull(opts, deps) {
2483
2610
  if (opts.json) {
2484
2611
  console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
2485
2612
  } else {
2486
- 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}`));
2487
2614
  }
2488
2615
  process.exitCode = 0;
2489
2616
  } catch (err) {
@@ -2522,7 +2649,7 @@ function escapeEnvValue(value) {
2522
2649
  function printError4(opts, err) {
2523
2650
  const message = err instanceof Error ? err.message : String(err);
2524
2651
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2525
- console.error(chalk17.red(`error: ${message}`));
2652
+ console.error(chalk18.red(`error: ${message}`));
2526
2653
  }
2527
2654
 
2528
2655
  // src/commands/credentials.ts
@@ -2534,7 +2661,7 @@ import {
2534
2661
  } from "fs/promises";
2535
2662
  import { mkdirSync } from "fs";
2536
2663
  import { basename as basename2, extname, join } from "path";
2537
- import chalk18 from "chalk";
2664
+ import chalk19 from "chalk";
2538
2665
  import { input, password as password2 } from "@inquirer/prompts";
2539
2666
  function clientFor2(opts) {
2540
2667
  const { token, baseUrl } = resolveAuth(opts);
@@ -2607,7 +2734,7 @@ function defaultDeps6(client) {
2607
2734
  mkdir: (dir) => mkdirSync(dir, { recursive: true }),
2608
2735
  writeFile: (path, data, mode) => writeFileAsync(path, data, mode !== void 0 ? { mode } : void 0),
2609
2736
  chmod: (path, mode) => chmodAsync(path, mode),
2610
- warn: (message) => console.error(chalk18.yellow(`warning: ${message}`))
2737
+ warn: (message) => console.error(chalk19.yellow(`warning: ${message}`))
2611
2738
  };
2612
2739
  }
2613
2740
  var ANDROID_KIND = "android_keystore";
@@ -2676,7 +2803,7 @@ async function credentialsAdd(opts, deps) {
2676
2803
  if (opts.json) {
2677
2804
  console.log(JSON.stringify(result));
2678
2805
  } else {
2679
- console.log(chalk18.green(`\u2713 added ${result.name} (${result.platform})`));
2806
+ console.log(chalk19.green(`\u2713 added ${result.name} (${result.platform})`));
2680
2807
  }
2681
2808
  process.exitCode = 0;
2682
2809
  } catch (err) {
@@ -2734,20 +2861,20 @@ async function credentialsGenerate(opts, deps) {
2734
2861
  }
2735
2862
  if (opts.json) {
2736
2863
  } else {
2737
- console.log(chalk18.green(`\u2713 generated ${result.name} (android)`));
2864
+ console.log(chalk19.green(`\u2713 generated ${result.name} (android)`));
2738
2865
  console.log("");
2739
- console.log(` Keystore saved to: ${chalk18.bold(savedTo)}`);
2740
- console.log(` Key alias: ${chalk18.bold(result.key_alias)}`);
2741
- console.log(` Store password: ${chalk18.bold(result.store_password)}`);
2742
- 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)}`);
2743
2870
  console.log("");
2744
2871
  console.log(
2745
- chalk18.yellow(
2872
+ chalk19.yellow(
2746
2873
  " These are shown ONCE \u2014 VibeView cannot recover the keystore file or its passwords. Back them up somewhere safe."
2747
2874
  )
2748
2875
  );
2749
2876
  console.log(
2750
- chalk18.yellow(
2877
+ chalk19.yellow(
2751
2878
  " The credential is already uploaded and active for android and androidtv production builds."
2752
2879
  )
2753
2880
  );
@@ -2773,19 +2900,19 @@ async function generateAppleCredential(platform, opts, d) {
2773
2900
  if (opts.json) {
2774
2901
  console.log(JSON.stringify(result));
2775
2902
  } else {
2776
- console.log(chalk18.green(`\u2713 generated ${result.name} (${result.platform})`));
2903
+ console.log(chalk19.green(`\u2713 generated ${result.name} (${result.platform})`));
2777
2904
  console.log("");
2778
- console.log(` Scope: ${chalk18.bold(result.app_public_id ?? "org-wide")}`);
2905
+ console.log(` Scope: ${chalk19.bold(result.app_public_id ?? "org-wide")}`);
2779
2906
  const expires = formatExpires(result);
2780
2907
  if (expires) {
2781
- console.log(` Expires: ${chalk18.bold(expires)}`);
2908
+ console.log(` Expires: ${chalk19.bold(expires)}`);
2782
2909
  }
2783
2910
  console.log("");
2784
2911
  console.log(
2785
2912
  ` The certificate and provisioning profile are stored with VibeView and already active for ${result.platform} production builds.`
2786
2913
  );
2787
2914
  console.log(
2788
- ` 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.`
2789
2916
  );
2790
2917
  }
2791
2918
  process.exitCode = 0;
@@ -2851,17 +2978,17 @@ async function credentialsDownload(name, opts, deps) {
2851
2978
  await d.chmod(path, 384);
2852
2979
  }
2853
2980
  if (!opts.json) {
2854
- console.log(chalk18.green(`\u2713 downloaded ${material.name} (${material.platform})`));
2981
+ console.log(chalk19.green(`\u2713 downloaded ${material.name} (${material.platform})`));
2855
2982
  console.log("");
2856
- console.log(` Saved to: ${chalk18.bold(savedTo)}`);
2983
+ console.log(` Saved to: ${chalk19.bold(savedTo)}`);
2857
2984
  if (profileSavedTo !== null) {
2858
- console.log(` Profile: ${chalk18.bold(profileSavedTo)}`);
2985
+ console.log(` Profile: ${chalk19.bold(profileSavedTo)}`);
2859
2986
  }
2860
2987
  for (const [key, value] of Object.entries(material.passwords)) {
2861
- console.log(` ${key}: ${chalk18.bold(value)}`);
2988
+ console.log(` ${key}: ${chalk19.bold(value)}`);
2862
2989
  }
2863
2990
  console.log("");
2864
- 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."));
2865
2992
  }
2866
2993
  process.exitCode = 0;
2867
2994
  } catch (err) {
@@ -2892,13 +3019,13 @@ async function ascKeyAdd(opts, deps) {
2892
3019
  if (opts.json) {
2893
3020
  console.log(JSON.stringify(result));
2894
3021
  } else {
2895
- 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}`));
2896
3023
  console.log("");
2897
- console.log(` Key ID: ${chalk18.bold(result.key_id)}`);
2898
- 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)}`);
2899
3026
  console.log("");
2900
3027
  console.log(
2901
- chalk18.yellow(
3028
+ chalk19.yellow(
2902
3029
  " The .p8 is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2903
3030
  )
2904
3031
  );
@@ -2933,7 +3060,7 @@ function formatAscKeysHuman(keys) {
2933
3060
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2934
3061
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2935
3062
  const join4 = (cells) => cells.join(" ").trimEnd();
2936
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
3063
+ const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
2937
3064
  for (const row of rows) lines.push(join4(padCells(row)));
2938
3065
  return lines.join("\n") + "\n";
2939
3066
  }
@@ -2944,7 +3071,7 @@ async function ascKeyRm(name, opts, deps) {
2944
3071
  if (opts.json) {
2945
3072
  console.log(JSON.stringify({ event: "deleted", name }));
2946
3073
  } else {
2947
- console.log(chalk18.green(`\u2713 removed App Store Connect key ${name}`));
3074
+ console.log(chalk19.green(`\u2713 removed App Store Connect key ${name}`));
2948
3075
  }
2949
3076
  process.exitCode = 0;
2950
3077
  } catch (err) {
@@ -2969,12 +3096,12 @@ async function playKeyAdd(opts, deps) {
2969
3096
  if (opts.json) {
2970
3097
  console.log(JSON.stringify(result));
2971
3098
  } else {
2972
- console.log(chalk18.green(`\u2713 added Google Play key ${result.name}`));
3099
+ console.log(chalk19.green(`\u2713 added Google Play key ${result.name}`));
2973
3100
  console.log("");
2974
- console.log(` Service account: ${chalk18.bold(result.client_email)}`);
3101
+ console.log(` Service account: ${chalk19.bold(result.client_email)}`);
2975
3102
  console.log("");
2976
3103
  console.log(
2977
- chalk18.yellow(
3104
+ chalk19.yellow(
2978
3105
  " The JSON key is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2979
3106
  )
2980
3107
  );
@@ -3009,7 +3136,7 @@ function formatPlayKeysHuman(keys) {
3009
3136
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3010
3137
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3011
3138
  const join4 = (cells) => cells.join(" ").trimEnd();
3012
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
3139
+ const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3013
3140
  for (const row of rows) lines.push(join4(padCells(row)));
3014
3141
  return lines.join("\n") + "\n";
3015
3142
  }
@@ -3020,7 +3147,7 @@ async function playKeyRm(name, opts, deps) {
3020
3147
  if (opts.json) {
3021
3148
  console.log(JSON.stringify({ event: "deleted", name }));
3022
3149
  } else {
3023
- console.log(chalk18.green(`\u2713 removed Google Play key ${name}`));
3150
+ console.log(chalk19.green(`\u2713 removed Google Play key ${name}`));
3024
3151
  }
3025
3152
  process.exitCode = 0;
3026
3153
  } catch (err) {
@@ -3089,7 +3216,7 @@ function formatCredentialsHuman(credentials2) {
3089
3216
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3090
3217
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3091
3218
  const join4 = (cells) => cells.join(" ").trimEnd();
3092
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
3219
+ const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3093
3220
  for (const row of rows) lines.push(join4(padCells(row)));
3094
3221
  return lines.join("\n") + "\n";
3095
3222
  }
@@ -3100,7 +3227,7 @@ async function credentialsRm(name, opts, deps) {
3100
3227
  if (opts.json) {
3101
3228
  console.log(JSON.stringify({ event: "deleted", name }));
3102
3229
  } else {
3103
- console.log(chalk18.green(`\u2713 removed ${name}`));
3230
+ console.log(chalk19.green(`\u2713 removed ${name}`));
3104
3231
  }
3105
3232
  process.exitCode = 0;
3106
3233
  } catch (err) {
@@ -3112,16 +3239,16 @@ function printKeystoreWriteFailure(opts, err, result) {
3112
3239
  const message = err instanceof Error ? err.message : String(err);
3113
3240
  if (opts.json) {
3114
3241
  console.log(JSON.stringify({ event: "error", message }));
3115
- console.error(chalk18.red(`error: ${message}`));
3242
+ console.error(chalk19.red(`error: ${message}`));
3116
3243
  return;
3117
3244
  }
3118
- console.error(chalk18.red(`error: ${message}`));
3245
+ console.error(chalk19.red(`error: ${message}`));
3119
3246
  console.error(
3120
- chalk18.red(
3247
+ chalk19.red(
3121
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.`
3122
3249
  )
3123
3250
  );
3124
- 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:"));
3125
3252
  console.error("");
3126
3253
  console.error(` Key alias: ${result.key_alias}`);
3127
3254
  console.error(` Store password: ${result.store_password}`);
@@ -3135,11 +3262,11 @@ function printKeystoreWriteFailure(opts, err, result) {
3135
3262
  function printError5(opts, err) {
3136
3263
  const message = err instanceof Error ? err.message : String(err);
3137
3264
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3138
- console.error(chalk18.red(`error: ${message}`));
3265
+ console.error(chalk19.red(`error: ${message}`));
3139
3266
  }
3140
3267
 
3141
3268
  // src/commands/webhooks.ts
3142
- import chalk19 from "chalk";
3269
+ import chalk20 from "chalk";
3143
3270
  function clientFor3(opts) {
3144
3271
  const { token, baseUrl } = resolveAuth(opts);
3145
3272
  return new ApiClient(baseUrl, token);
@@ -3170,14 +3297,14 @@ async function webhooksAdd(opts, deps) {
3170
3297
  if (opts.json) {
3171
3298
  console.log(JSON.stringify(result));
3172
3299
  } else {
3173
- console.log(chalk19.green(`\u2713 added ${result.id} ${result.url}`));
3300
+ console.log(chalk20.green(`\u2713 added ${result.id} ${result.url}`));
3174
3301
  console.log("");
3175
- console.log(` Signing secret: ${chalk19.bold(result.secret)}`);
3302
+ console.log(` Signing secret: ${chalk20.bold(result.secret)}`);
3176
3303
  if (opts.secret !== void 0) {
3177
- console.log(chalk19.yellow(" (This is the secret you provided.)"));
3304
+ console.log(chalk20.yellow(" (This is the secret you provided.)"));
3178
3305
  }
3179
3306
  console.log(
3180
- 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.")
3181
3308
  );
3182
3309
  }
3183
3310
  process.exitCode = 0;
@@ -3208,7 +3335,7 @@ function formatWebhooksHuman(webhooks2) {
3208
3335
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3209
3336
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3210
3337
  const join4 = (cells) => cells.join(" ").trimEnd();
3211
- const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3338
+ const lines = [chalk20.bold(join4(padCells(HEADERS3)))];
3212
3339
  for (const row of rows) lines.push(join4(padCells(row)));
3213
3340
  return lines.join("\n") + "\n";
3214
3341
  }
@@ -3234,7 +3361,7 @@ async function webhooksRm(id, opts, deps) {
3234
3361
  if (opts.json) {
3235
3362
  console.log(JSON.stringify({ event: "deleted", id }));
3236
3363
  } else {
3237
- console.log(chalk19.green(`\u2713 removed ${id}`));
3364
+ console.log(chalk20.green(`\u2713 removed ${id}`));
3238
3365
  }
3239
3366
  process.exitCode = 0;
3240
3367
  } catch (err) {
@@ -3267,13 +3394,13 @@ async function webhooksTest(id, opts, deps) {
3267
3394
  if (opts.json) console.log(JSON.stringify(ld));
3268
3395
  if (ld.status === "succeeded") {
3269
3396
  if (!opts.json) {
3270
- console.log(chalk19.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3397
+ console.log(chalk20.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3271
3398
  }
3272
3399
  process.exitCode = 0;
3273
3400
  } else {
3274
3401
  if (!opts.json) {
3275
3402
  const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
3276
- console.error(chalk19.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3403
+ console.error(chalk20.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3277
3404
  }
3278
3405
  process.exitCode = 1;
3279
3406
  }
@@ -3282,7 +3409,7 @@ async function webhooksTest(id, opts, deps) {
3282
3409
  }
3283
3410
  if (opts.json) console.log(JSON.stringify({ status: "pending" }));
3284
3411
  console.error(
3285
- chalk19.yellow(
3412
+ chalk20.yellow(
3286
3413
  `Delivery still pending after ${TEST_POLL_BUDGET_MS / 1e3}s \u2014 check \`vibeview webhooks list\` later for the result.`
3287
3414
  )
3288
3415
  );
@@ -3295,7 +3422,7 @@ async function webhooksTest(id, opts, deps) {
3295
3422
  function printError6(opts, err) {
3296
3423
  const message = err instanceof Error ? err.message : String(err);
3297
3424
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3298
- console.error(chalk19.red(`error: ${message}`));
3425
+ console.error(chalk20.red(`error: ${message}`));
3299
3426
  }
3300
3427
 
3301
3428
  // src/commands/list-devices.ts
@@ -3393,7 +3520,7 @@ import { dirname, join as join2 } from "path";
3393
3520
  import { fileURLToPath } from "url";
3394
3521
  import { spawnSync } from "child_process";
3395
3522
  import { confirm } from "@inquirer/prompts";
3396
- import chalk20 from "chalk";
3523
+ import chalk21 from "chalk";
3397
3524
  function resolveShippedSkillPath(fromDir) {
3398
3525
  for (const rel of ["..", join2("..", "..")]) {
3399
3526
  const candidate = join2(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
@@ -3421,23 +3548,23 @@ function registerMcp() {
3421
3548
  encoding: "utf8"
3422
3549
  });
3423
3550
  if (res.status === 0) {
3424
- 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)"));
3425
3552
  return;
3426
3553
  }
3427
3554
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
3428
3555
  if (out.includes("already exists")) {
3429
- 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"));
3430
3557
  return;
3431
3558
  }
3432
- console.error(chalk20.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
3433
- 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"));
3434
3561
  }
3435
3562
  async function agentSetup(opts) {
3436
3563
  const moduleDir = dirname(fileURLToPath(import.meta.url));
3437
3564
  const shipped = resolveShippedSkillPath(moduleDir);
3438
3565
  if (!shipped) {
3439
3566
  console.error(
3440
- chalk20.red(
3567
+ chalk21.red(
3441
3568
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
3442
3569
  )
3443
3570
  );
@@ -3446,9 +3573,9 @@ async function agentSetup(opts) {
3446
3573
  }
3447
3574
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
3448
3575
  const result = installSkill(shipped, targetDir);
3449
- 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")}`));
3450
3577
  console.log(
3451
- chalk20.dim(
3578
+ chalk21.dim(
3452
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."
3453
3580
  )
3454
3581
  );
@@ -3456,7 +3583,7 @@ async function agentSetup(opts) {
3456
3583
  if (!claudeCliAvailable()) {
3457
3584
  if (opts.mcp === true) {
3458
3585
  console.error(
3459
- chalk20.yellow(
3586
+ chalk21.yellow(
3460
3587
  "\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
3461
3588
  )
3462
3589
  );
@@ -3470,7 +3597,7 @@ async function agentSetup(opts) {
3470
3597
  }
3471
3598
  console.log(
3472
3599
  `
3473
- 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(
3474
3601
  "AGENTS.md"
3475
3602
  )} for other agents):
3476
3603
  `
@@ -3478,7 +3605,7 @@ Suggested line for your project's ${chalk20.bold("CLAUDE.md")} (or ${chalk20.bol
3478
3605
  console.log(` ${CLAUDE_MD_SUGGESTION}
3479
3606
  `);
3480
3607
  console.log(
3481
- chalk20.dim(
3608
+ chalk21.dim(
3482
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"
3483
3610
  )
3484
3611
  );
@@ -3613,6 +3740,7 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
3613
3740
  }
3614
3741
  try {
3615
3742
  parseRunPlatform(opts.platform);
3743
+ refusePlatformWithApp(opts);
3616
3744
  } catch (err) {
3617
3745
  process.stderr.write(`error: ${err.message}
3618
3746
  `);
@@ -3681,6 +3809,7 @@ program.command("run-test <test-id>").description("Run a single test case").opti
3681
3809
  }
3682
3810
  try {
3683
3811
  parseRunPlatform(opts.platform);
3812
+ refusePlatformWithApp(opts);
3684
3813
  } catch (err) {
3685
3814
  process.stderr.write(`error: ${err.message}
3686
3815
  `);
@@ -3899,7 +4028,7 @@ program.command("dev-reload").description(
3899
4028
  program.command("mcp").description(
3900
4029
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
3901
4030
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
3902
- const { runMcpServer } = await import("./server-2JNYWF5L.js");
4031
+ const { runMcpServer } = await import("./server-GPFOZOFL.js");
3903
4032
  await runMcpServer({ ...opts, version: pkg.version });
3904
4033
  });
3905
4034
  program.command("agent-setup").description(