vibeview 0.30.0 → 0.32.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
@@ -21,9 +21,10 @@ import {
21
21
  printOutcomeLine,
22
22
  printTransition,
23
23
  readDevState,
24
+ readDevStateFile,
24
25
  readMetroState,
25
26
  runAgentVerb
26
- } from "./chunk-NOZGZT3G.js";
27
+ } from "./chunk-T6H5XTBX.js";
27
28
  import {
28
29
  ApiClient,
29
30
  ApiStatusError,
@@ -51,7 +52,7 @@ import { createRequire } from "module";
51
52
  import { Command, Option } from "commander";
52
53
 
53
54
  // src/commands/run-suite.ts
54
- import chalk3 from "chalk";
55
+ import chalk4 from "chalk";
55
56
 
56
57
  // src/lib/poll.ts
57
58
  import chalk2 from "chalk";
@@ -145,6 +146,10 @@ function formatHuman(result, verbose) {
145
146
  lines.push(`
146
147
  ${chalk.red("Error:")} ${result.error_message}`);
147
148
  }
149
+ if (result.build_warning && result.status !== "passed") {
150
+ lines.push(`
151
+ ${chalk.yellow("Note:")} ${result.build_warning}`);
152
+ }
148
153
  lines.push("");
149
154
  return lines.join("\n");
150
155
  }
@@ -174,9 +179,16 @@ function formatJunit(result) {
174
179
  lines.push(
175
180
  ` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}" timestamp="${timestamp}">`
176
181
  );
177
- if (result.url) {
182
+ if (result.url || result.build_warning) {
178
183
  lines.push(" <properties>");
179
- lines.push(` <property name="vibeview.run_url" value="${escapeXml(result.url)}" />`);
184
+ if (result.url) {
185
+ lines.push(` <property name="vibeview.run_url" value="${escapeXml(result.url)}" />`);
186
+ }
187
+ if (result.build_warning) {
188
+ lines.push(
189
+ ` <property name="vibeview.build_warning" value="${escapeXml(result.build_warning)}" />`
190
+ );
191
+ }
180
192
  lines.push(" </properties>");
181
193
  }
182
194
  for (const tr of cases) {
@@ -225,46 +237,97 @@ function formatJunitError(name, message, url) {
225
237
  return lines.join("\n");
226
238
  }
227
239
  var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
240
+ function caseLines(c, classname) {
241
+ const name = escapeXml(c.name);
242
+ const head = ` <testcase name="${name}" classname="${classname}" time="${c.time}"`;
243
+ const out = c.url ? ` <system-out>${escapeXml(c.url)}</system-out>` : null;
244
+ if (c.verdict === "passed" && !out) return [`${head} />`];
245
+ const lines = [`${head}>`];
246
+ const msg = escapeXml(c.message ?? "");
247
+ if (c.verdict === "failure") {
248
+ lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
249
+ } else if (c.verdict === "error") {
250
+ lines.push(` <error message="${msg}" type="InfraError">${msg}</error>`);
251
+ } else if (c.verdict === "skipped") {
252
+ lines.push(` <skipped message="${msg}" />`);
253
+ }
254
+ if (out) lines.push(out);
255
+ lines.push(" </testcase>");
256
+ return lines;
257
+ }
228
258
  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);
259
+ const unfinished = (status) => opts.timedOut !== void 0 && !MULTI_TERMINAL.has(status ?? "");
235
260
  const timestamp = result.started_at ?? (/* @__PURE__ */ new Date()).toISOString();
261
+ const suites = result.suite_runs.map((sr) => {
262
+ const suiteName = sr.suite_name || sr.suite_id;
263
+ const cases = (sr.case_runs ?? []).map((c) => {
264
+ const name = c.case_name || c.run_id;
265
+ const time = c.duration_ms ? (c.duration_ms / 1e3).toFixed(1) : "0";
266
+ const base = { name, time, url: c.url || void 0 };
267
+ switch (c.status) {
268
+ case "passed":
269
+ return { ...base, verdict: "passed" };
270
+ case "failed":
271
+ return {
272
+ ...base,
273
+ verdict: "failure",
274
+ message: c.error_message || "Test failed"
275
+ };
276
+ case "error":
277
+ return {
278
+ ...base,
279
+ verdict: "error",
280
+ message: c.error_message || "Infrastructure error"
281
+ };
282
+ case "cancelled":
283
+ return { ...base, verdict: "skipped", message: c.error_message || "Cancelled" };
284
+ default:
285
+ return unfinished(c.status) ? {
286
+ ...base,
287
+ verdict: "error",
288
+ message: `${opts.timedOut} (case was ${c.status ?? "not started"})`
289
+ } : { ...base, verdict: "skipped", message: "Not run" };
290
+ }
291
+ });
292
+ const suiteError = unfinished(sr.status) ? `${opts.timedOut} (suite was ${sr.status})` : sr.status === "error" ? sr.error_message || "Suite errored" : null;
293
+ if (suiteError && !cases.some((c) => c.verdict === "error")) {
294
+ cases.push({ name: suiteName, time: "0", verdict: "error", message: suiteError });
295
+ }
296
+ const legacy = sr.case_runs === void 0;
297
+ const tests2 = legacy ? sr.passed + sr.failed + (suiteError ? 1 : 0) : cases.length;
298
+ const failures = legacy ? sr.failed : cases.filter((c) => c.verdict === "failure").length;
299
+ const errors2 = cases.filter((c) => c.verdict === "error").length;
300
+ const skipped = cases.filter((c) => c.verdict === "skipped").length;
301
+ return { sr, suiteName, cases, tests: tests2, failures, errors: errors2, skipped };
302
+ });
303
+ const noSuites = opts.timedOut !== void 0 && result.suite_runs.length === 0;
304
+ const sum = (key) => suites.reduce((n, s) => n + s[key], 0);
305
+ const tests = sum("tests") + (noSuites ? 1 : 0);
306
+ const errors = sum("errors") + (noSuites ? 1 : 0);
236
307
  const lines = [];
237
308
  lines.push('<?xml version="1.0" encoding="UTF-8"?>');
238
309
  lines.push(
239
- `<testsuites name="vibeview-multi" tests="${tests}" failures="${failures}" errors="${totalErrors}">`
310
+ `<testsuites name="vibeview-multi" tests="${tests}" failures="${sum("failures")}" errors="${errors}" skipped="${sum("skipped")}">`
240
311
  );
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;
312
+ for (const s of suites) {
313
+ const suiteName = escapeXml(s.suiteName);
246
314
  lines.push(
247
- ` <testsuite name="${suiteName}" tests="${suiteTests}" failures="${suiteFails}" errors="${suiteErrors}" timestamp="${timestamp}">`
315
+ ` <testsuite name="${suiteName}" tests="${s.tests}" failures="${s.failures}" errors="${s.errors}" skipped="${s.skipped}" timestamp="${timestamp}">`
248
316
  );
249
- if (sr.url) {
317
+ if (s.sr.url) {
250
318
  lines.push(" <properties>");
251
- lines.push(` <property name="vibeview.run_url" value="${escapeXml(sr.url)}" />`);
319
+ lines.push(` <property name="vibeview.run_url" value="${escapeXml(s.sr.url)}" />`);
252
320
  lines.push(" </properties>");
253
321
  }
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
- }
322
+ for (const c of s.cases) lines.push(...caseLines(c, suiteName));
260
323
  lines.push(" </testsuite>");
261
324
  }
262
325
  if (noSuites) {
263
- const msg = escapeXml(opts.timedOut);
326
+ const name = "vibeview run-suites";
264
327
  lines.push(
265
- ` <testsuite name="vibeview run-suites" tests="0" failures="0" errors="1" timestamp="${timestamp}">`
328
+ ` <testsuite name="${name}" tests="1" failures="0" errors="1" skipped="0" timestamp="${timestamp}">`
266
329
  );
267
- lines.push(` <error message="${msg}" type="InfraError" />`);
330
+ lines.push(...caseLines({ name, time: "0", verdict: "error", message: opts.timedOut }, name));
268
331
  lines.push(" </testsuite>");
269
332
  }
270
333
  lines.push("</testsuites>");
@@ -481,15 +544,20 @@ function formatAction(action) {
481
544
  function formatToolCall(tc) {
482
545
  return formatAction({ tool: tc.name, args: tc.args });
483
546
  }
547
+ function runStartedLine(evt) {
548
+ const steps = `${evt.total_steps} step(s)`;
549
+ const model = chalk2.bold(evt.model || "default");
550
+ if (evt.mode === "replay") return `${chalk2.cyan("\u25B6")} Replay started \u2014 ${steps} (no AI)`;
551
+ if (evt.mode === "hybrid") {
552
+ return `${chalk2.cyan("\u25B6")} Hybrid test started \u2014 ${steps}, replaying the recording, AI fallback model: ${model}`;
553
+ }
554
+ return `${chalk2.cyan("\u25B6")} AI test started \u2014 ${steps}, model: ${model}`;
555
+ }
484
556
  function displayProgressEvent(evt, ctx) {
485
557
  const { spinner } = ctx;
486
558
  switch (evt.event) {
487
559
  case "run_started":
488
- printBelowSpinner(
489
- spinner,
490
- `${chalk2.cyan("\u25B6")} AI test started \u2014 ${evt.total_steps} step(s), model: ${chalk2.bold(evt.model || "default")}`,
491
- " Starting test..."
492
- );
560
+ printBelowSpinner(spinner, runStartedLine(evt), " Starting test...");
493
561
  ctx.totalSteps = evt.total_steps ?? ctx.totalSteps;
494
562
  break;
495
563
  case "step_started": {
@@ -572,6 +640,7 @@ function mapExitCode(status) {
572
640
  }
573
641
 
574
642
  // src/lib/run-target.ts
643
+ import chalk3 from "chalk";
575
644
  var RUN_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
576
645
  function parseRunPlatform(value) {
577
646
  if (value === void 0) return void 0;
@@ -580,6 +649,18 @@ function parseRunPlatform(value) {
580
649
  }
581
650
  return value;
582
651
  }
652
+ function refusePlatformWithApp(options) {
653
+ if (options.platform && options.app) {
654
+ throw new Error(
655
+ "--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."
656
+ );
657
+ }
658
+ }
659
+ function printBuildWarning(warning, label) {
660
+ if (!warning) return;
661
+ const prefix = label ? `warning (${label}):` : "warning:";
662
+ process.stderr.write(chalk3.yellow(`${prefix} ${warning}`) + "\n");
663
+ }
583
664
  function describeRunTarget(response, options) {
584
665
  const asked = Boolean(options.platform || options.app);
585
666
  if (!response.app_id) {
@@ -668,12 +749,13 @@ ${signal} received.
668
749
  const target = describeRunTarget(response, options);
669
750
  spinner.stop();
670
751
  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}
752
+ `${chalk4.dim("Suite:")} ${suiteId}
753
+ ` + (target ? `${chalk4.dim("App:")} ${target}
754
+ ` : "") + `${chalk4.dim("Run:")} ${response.run_id}
755
+ ` + (sessionId ? `${chalk4.dim("Session:")} ${sessionId}
675
756
  ` : "")
676
757
  );
758
+ printBuildWarning(response.build_warning);
677
759
  spinner.start("Waiting for device...");
678
760
  const timeoutMs = (options.timeout ?? 300) * 1e3;
679
761
  const result = await pollUntilComplete(
@@ -691,7 +773,10 @@ ${signal} received.
691
773
  process.stdin.setRawMode(false);
692
774
  process.stdin.pause();
693
775
  }
694
- outputResult(result, options);
776
+ outputResult(
777
+ response.build_warning ? { ...result, build_warning: response.build_warning } : result,
778
+ options
779
+ );
695
780
  process.exit(mapExitCode(result.status));
696
781
  } catch (err) {
697
782
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
@@ -717,7 +802,7 @@ ${signal} received.
717
802
  }
718
803
 
719
804
  // src/commands/run-suites.ts
720
- import chalk4 from "chalk";
805
+ import chalk5 from "chalk";
721
806
  var AGGREGATE_TIMEOUT_CAP_S = 7200;
722
807
  async function runSuites(opts) {
723
808
  if (!opts.tag || opts.tag.length === 0) {
@@ -792,13 +877,13 @@ ${signal} received.
792
877
  const noMatch = `No suites match tags: ${opts.tag.join(", ")}${suffix}`;
793
878
  if (opts.output === "json") printJsonError(noMatch);
794
879
  if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", noMatch));
795
- process.stderr.write(chalk4.yellow(`${noMatch}
880
+ process.stderr.write(chalk5.yellow(`${noMatch}
796
881
  `));
797
882
  process.exit(EXIT_INFRA_ERROR);
798
883
  }
799
884
  if (opts.output === "json") printJsonError(err);
800
885
  if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", msg));
801
- process.stderr.write(chalk4.red(`Failed to start: ${msg}
886
+ process.stderr.write(chalk5.red(`Failed to start: ${msg}
802
887
  `));
803
888
  process.exit(EXIT_INFRA_ERROR);
804
889
  }
@@ -807,16 +892,17 @@ ${signal} received.
807
892
  const matchedNames = start.matched_suites?.map((s) => s.name) ?? [];
808
893
  const totalMatched = start.matched_suite_ids.length;
809
894
  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(", ") : ""}
895
+ `${chalk5.dim("Aggregate:")} ${start.aggregate_id}
896
+ ${chalk5.dim("Tags:")} ${opts.tag.join(", ")} ${chalk5.dim("(" + (opts.match ?? "any") + ")")}
897
+ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u2014 " + matchedNames.join(", ") : ""}
813
898
  `
814
899
  );
900
+ for (const s of start.matched_suites ?? []) printBuildWarning(s.build_warning, s.name);
815
901
  const excluded = start.excluded_suites ?? [];
816
902
  if (excluded.length) {
817
903
  const names = excluded.map((s) => s.name).join(", ");
818
904
  process.stderr.write(
819
- chalk4.dim(`Excluded ${excluded.length} suite(s) (platform mismatch): ${names}
905
+ chalk5.dim(`Excluded ${excluded.length} suite(s) (platform mismatch): ${names}
820
906
  `)
821
907
  );
822
908
  }
@@ -830,20 +916,28 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
830
916
  const suiteStateBefore = /* @__PURE__ */ new Map();
831
917
  const suiteStartedPrinted = /* @__PURE__ */ new Set();
832
918
  let lastQueueReason = null;
919
+ let pending = [];
833
920
  const print = (line) => {
921
+ pending.push(line);
922
+ };
923
+ const flush = (nextText) => {
924
+ const textChanged = nextText !== null && nextText !== spinner.text;
925
+ if (pending.length === 0 && !textChanged) return;
834
926
  spinner.stop();
835
- process.stderr.write(line + "\n");
836
- spinner.start();
927
+ for (const line of pending) process.stderr.write(line + "\n");
928
+ pending = [];
929
+ if (nextText !== null) spinner.start(nextText);
837
930
  };
838
931
  const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
839
932
  let last = null;
840
933
  const abandon = async (reason) => {
934
+ flush(null);
841
935
  spinner.stop();
842
936
  if (process.stdin.isTTY) {
843
937
  process.stdin.setRawMode(false);
844
938
  process.stdin.pause();
845
939
  }
846
- process.stderr.write(chalk4.red(`${reason}
940
+ process.stderr.write(chalk5.red(`${reason}
847
941
  `));
848
942
  process.stderr.write("Cancelling multi-suite run...\n");
849
943
  try {
@@ -851,7 +945,7 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
851
945
  process.stderr.write("Cancellation requested.\n");
852
946
  } catch (err) {
853
947
  const m = err instanceof Error ? err.message : String(err);
854
- process.stderr.write(chalk4.yellow(`Could not cancel the run: ${m}
948
+ process.stderr.write(chalk5.yellow(`Could not cancel the run: ${m}
855
949
  `));
856
950
  }
857
951
  if (opts.output === "json") {
@@ -879,6 +973,7 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
879
973
  return abandon(`Could not read the run's status: ${m}`);
880
974
  }
881
975
  let doneCount = 0;
976
+ let nextText;
882
977
  let queuedChild;
883
978
  for (const sr of last.suite_runs) {
884
979
  const cur = sr.status;
@@ -888,17 +983,17 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
888
983
  }
889
984
  if (cur === "running" && !suiteStartedPrinted.has(sr.run_id)) {
890
985
  suiteStartedPrinted.add(sr.run_id);
891
- print(`${chalk4.cyan("\u25B6")} ${chalk4.bold(sr.suite_name)} started`);
986
+ print(`${chalk5.cyan("\u25B6")} ${chalk5.bold(sr.suite_name)} started`);
892
987
  }
893
988
  const wasTerminal = prev !== void 0 && TERMINAL.has(prev);
894
989
  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) : "";
990
+ const mark = cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
991
+ const link = sr.url ? chalk5.dim(" " + sr.url) : "";
897
992
  print(
898
- `${mark} ${chalk4.bold(sr.suite_name)} ${cur}: ${sr.passed} passed, ${sr.failed} failed${link}`
993
+ `${mark} ${chalk5.bold(sr.suite_name)} ${cur}: ${sr.passed} passed, ${sr.failed} failed${link}`
899
994
  );
900
995
  if (sr.error_message) {
901
- print(` ${chalk4.red(sr.error_message)}`);
996
+ print(` ${chalk5.red(sr.error_message)}`);
902
997
  }
903
998
  }
904
999
  suiteStateBefore.set(sr.run_id, cur);
@@ -911,16 +1006,20 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
911
1006
  const why = queueReasonCopy(q.reason);
912
1007
  if (why && why !== lastQueueReason) {
913
1008
  print(
914
- `${chalk4.yellow("\u23F3")} ${chalk4.bold(queuedChild.suite_name)} is waiting for a device: ${why}`
1009
+ `${chalk5.yellow("\u23F3")} ${chalk5.bold(queuedChild.suite_name)} is waiting for a device: ${why}`
915
1010
  );
916
1011
  }
917
1012
  lastQueueReason = why;
918
- spinner.text = `Queued \u2014 ${chalk4.bold(queuedChild.suite_name)} position ${q.position} for ${target} (${doneCount}/${totalMatched} suite(s) complete)`;
1013
+ nextText = `Queued \u2014 ${chalk5.bold(queuedChild.suite_name)} position ${q.position} for ${target} (${doneCount}/${totalMatched} suite(s) complete)`;
919
1014
  } else {
920
1015
  lastQueueReason = null;
921
- spinner.text = `Running suites \u2014 ${doneCount}/${totalMatched} complete...`;
1016
+ nextText = `Running suites \u2014 ${doneCount}/${totalMatched} complete...`;
922
1017
  }
923
- if (TERMINAL.has(last.status)) break;
1018
+ if (TERMINAL.has(last.status)) {
1019
+ flush(null);
1020
+ break;
1021
+ }
1022
+ flush(nextText);
924
1023
  await new Promise((r) => setTimeout(r, 2e3));
925
1024
  }
926
1025
  if (!last || !TERMINAL.has(last.status)) {
@@ -934,7 +1033,9 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
934
1033
  process.stdin.pause();
935
1034
  }
936
1035
  if (opts.output === "json") {
937
- process.stdout.write(JSON.stringify(last, null, 2) + "\n");
1036
+ 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 }));
1037
+ const doc = buildWarnings.length ? { ...last, build_warnings: buildWarnings } : last;
1038
+ process.stdout.write(JSON.stringify(doc, null, 2) + "\n");
938
1039
  } else if (opts.output === "junit") {
939
1040
  process.stdout.write(formatMultiSuiteJunit(last) + "\n");
940
1041
  } else {
@@ -942,25 +1043,25 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
942
1043
  Aggregate: ${last.status}
943
1044
  `);
944
1045
  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}`) : "";
1046
+ const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
1047
+ const link = sr.url ? chalk5.dim(` ${sr.url}`) : "";
947
1048
  process.stdout.write(
948
1049
  ` ${mark} ${sr.suite_name}: ${sr.passed} passed, ${sr.failed} failed${link}
949
1050
  `
950
1051
  );
951
1052
  if (sr.error_message) {
952
- process.stdout.write(` ${chalk4.red(sr.error_message)}
1053
+ process.stdout.write(` ${chalk5.red(sr.error_message)}
953
1054
  `);
954
1055
  }
955
1056
  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)`) : "";
1057
+ const caseMark = cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
1058
+ const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
958
1059
  process.stdout.write(
959
- ` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${dur} ${chalk4.dim(cr.url)}
1060
+ ` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${dur} ${chalk5.dim(cr.url)}
960
1061
  `
961
1062
  );
962
1063
  if (cr.error_message && cr.status !== "passed") {
963
- process.stdout.write(` ${chalk4.red(cr.error_message)}
1064
+ process.stdout.write(` ${chalk5.red(cr.error_message)}
964
1065
  `);
965
1066
  }
966
1067
  }
@@ -972,7 +1073,7 @@ Aggregate: ${last.status}
972
1073
  }
973
1074
 
974
1075
  // src/commands/run-test.ts
975
- import chalk5 from "chalk";
1076
+ import chalk6 from "chalk";
976
1077
  async function runTest(testId, options) {
977
1078
  const { token, baseUrl } = resolveAuth(options);
978
1079
  const client = new ApiClient(baseUrl, token);
@@ -1046,13 +1147,14 @@ ${signal} received.
1046
1147
  const target = describeRunTarget(response, options);
1047
1148
  spinner.stop();
1048
1149
  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}
1150
+ `${chalk6.dim("Session:")} ${sessionId || "unknown"}
1151
+ ${chalk6.dim("Device:")} ${response.device_name || "allocating..."}
1152
+ ${chalk6.dim("Test:")} ${testId}
1153
+ ` + (target ? `${chalk6.dim("App:")} ${target}
1154
+ ` : "") + `${chalk6.dim("Run:")} ${response.run_id}
1054
1155
  `
1055
1156
  );
1157
+ printBuildWarning(response.build_warning);
1056
1158
  spinner.start("Waiting for device...");
1057
1159
  const timeoutMs = (options.timeout ?? 300) * 1e3;
1058
1160
  const result = await pollUntilComplete(
@@ -1070,7 +1172,10 @@ ${chalk5.dim("Test:")} ${testId}
1070
1172
  process.stdin.setRawMode(false);
1071
1173
  process.stdin.pause();
1072
1174
  }
1073
- outputResult(result, options);
1175
+ outputResult(
1176
+ response.build_warning ? { ...result, build_warning: response.build_warning } : result,
1177
+ options
1178
+ );
1074
1179
  process.exit(mapExitCode(result.status));
1075
1180
  } catch (err) {
1076
1181
  spinner.fail(err instanceof Error ? err.message : "Unknown error");
@@ -1096,7 +1201,7 @@ ${chalk5.dim("Test:")} ${testId}
1096
1201
  }
1097
1202
 
1098
1203
  // src/commands/list-suites.ts
1099
- import chalk6 from "chalk";
1204
+ import chalk7 from "chalk";
1100
1205
  async function listSuites(options) {
1101
1206
  const { token, baseUrl } = resolveAuth(options);
1102
1207
  const client = new ApiClient(baseUrl, token);
@@ -1118,21 +1223,21 @@ async function listSuites(options) {
1118
1223
  console.log("No test suites found.");
1119
1224
  return;
1120
1225
  }
1121
- console.log(chalk6.bold(`
1226
+ console.log(chalk7.bold(`
1122
1227
  ${suites.length} suite(s)
1123
1228
  `));
1124
1229
  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(", ")}]`) : "";
1230
+ const platform = s.platform ? chalk7.dim(` [${s.platform}]`) : "";
1231
+ const tagsText = s.tags && s.tags.length > 0 ? chalk7.dim(` [${s.tags.map((t) => t.display_name).join(", ")}]`) : "";
1127
1232
  console.log(
1128
- ` ${chalk6.cyan(s.id)} ${s.name}${platform}${tagsText} (${s.case_count} ${s.case_count === 1 ? "case" : "cases"})`
1233
+ ` ${chalk7.cyan(s.id)} ${s.name}${platform}${tagsText} (${s.case_count} ${s.case_count === 1 ? "case" : "cases"})`
1129
1234
  );
1130
1235
  }
1131
1236
  console.log();
1132
1237
  }
1133
1238
 
1134
1239
  // src/commands/list-apps.ts
1135
- import chalk7 from "chalk";
1240
+ import chalk8 from "chalk";
1136
1241
  async function listApps(options) {
1137
1242
  const { token, baseUrl } = resolveAuth(options);
1138
1243
  const client = new ApiClient(baseUrl, token);
@@ -1151,20 +1256,20 @@ async function listApps(options) {
1151
1256
  console.log("No apps found.");
1152
1257
  return;
1153
1258
  }
1154
- console.log(chalk7.bold(`
1259
+ console.log(chalk8.bold(`
1155
1260
  ${apps.length} app(s)
1156
1261
  `));
1157
1262
  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}`));
1263
+ const platform = chalk8.dim(` [${app.platform}]`);
1264
+ const pkg2 = chalk8.dim(app.package_name);
1265
+ console.log(` ${chalk8.cyan(app.public_id)} ${app.name}${platform} ${pkg2}`);
1266
+ console.log(chalk8.dim(` ${baseUrl}/apps/${app.public_id}`));
1162
1267
  }
1163
1268
  console.log();
1164
1269
  }
1165
1270
 
1166
1271
  // src/commands/stop.ts
1167
- import chalk8 from "chalk";
1272
+ import chalk9 from "chalk";
1168
1273
  async function stopCommand(sessionId, opts, deps) {
1169
1274
  const id = sessionId?.trim();
1170
1275
  if (!id) throw new UsageError("a session id is required: vibeview stop <session-id>");
@@ -1175,7 +1280,7 @@ async function stopCommand(sessionId, opts, deps) {
1175
1280
  } catch (err) {
1176
1281
  const message = err instanceof Error ? err.message : String(err);
1177
1282
  if (opts.json) console.log(JSON.stringify({ stopped: false, session_id: id, message }));
1178
- console.error(chalk8.red(`error: ${message}`));
1283
+ console.error(chalk9.red(`error: ${message}`));
1179
1284
  process.exitCode = errorExitCode(err);
1180
1285
  return;
1181
1286
  }
@@ -1208,7 +1313,7 @@ async function stopCommand(sessionId, opts, deps) {
1208
1313
  }
1209
1314
 
1210
1315
  // src/commands/create-test.ts
1211
- import chalk9 from "chalk";
1316
+ import chalk10 from "chalk";
1212
1317
  import { existsSync, readFileSync } from "fs";
1213
1318
  async function readStdin() {
1214
1319
  return new Promise((resolve, reject) => {
@@ -1249,6 +1354,21 @@ async function resolveSteps(options) {
1249
1354
  }
1250
1355
  throw new Error("No steps provided. Use --steps, --steps-file, or pipe JSON to stdin.");
1251
1356
  }
1357
+ function testCaseUrl(baseUrl, publicId) {
1358
+ return `${baseUrl}/tests/cases/${encodeURIComponent(publicId)}`;
1359
+ }
1360
+ function assertHasSteps(steps) {
1361
+ if (!Array.isArray(steps)) {
1362
+ throw new Error(
1363
+ `Steps must be a JSON array of instructions, e.g. '["Open the app", "Verify the Home screen shows"]'.`
1364
+ );
1365
+ }
1366
+ if (steps.length === 0) {
1367
+ throw new Error(
1368
+ `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"]'.`
1369
+ );
1370
+ }
1371
+ }
1252
1372
  async function resolveAppId(client, appRef, platform) {
1253
1373
  if (appRef.includes(".")) {
1254
1374
  if (!platform) {
@@ -1270,6 +1390,7 @@ async function createTest(options) {
1270
1390
  const client = new ApiClient(baseUrl, token);
1271
1391
  try {
1272
1392
  const steps = await resolveSteps(options);
1393
+ assertHasSteps(steps);
1273
1394
  let appPublicId;
1274
1395
  if (options.app) {
1275
1396
  appPublicId = await resolveAppId(client, options.app, options.platform);
@@ -1295,7 +1416,7 @@ async function createTest(options) {
1295
1416
  name: result.name,
1296
1417
  app_id: appPublicId || null,
1297
1418
  outcome: result.outcome ?? null,
1298
- url: `${baseUrl}/tests?tab=standalone`
1419
+ url: testCaseUrl(baseUrl, result.public_id)
1299
1420
  },
1300
1421
  null,
1301
1422
  2
@@ -1303,20 +1424,20 @@ async function createTest(options) {
1303
1424
  );
1304
1425
  } else {
1305
1426
  console.log(`
1306
- ${chalk9.cyan(result.public_id)} ${result.name}`);
1427
+ ${chalk10.cyan(result.public_id)} ${result.name}`);
1307
1428
  if (result.app_name) {
1308
- console.log(` ${chalk9.dim(`App: ${result.app_name}`)}`);
1429
+ console.log(` ${chalk10.dim(`App: ${result.app_name}`)}`);
1309
1430
  }
1310
1431
  if (result.platform) {
1311
- console.log(` ${chalk9.dim(`Platform: ${result.platform}`)}`);
1432
+ console.log(` ${chalk10.dim(`Platform: ${result.platform}`)}`);
1312
1433
  }
1313
- console.log(` ${chalk9.dim(`Steps: ${steps.length}`)}`);
1314
- console.log(chalk9.dim(` ${baseUrl}/tests?tab=standalone
1434
+ console.log(` ${chalk10.dim(`Steps: ${steps.length}`)}`);
1435
+ console.log(chalk10.dim(` ${testCaseUrl(baseUrl, result.public_id)}
1315
1436
  `));
1316
1437
  }
1317
1438
  } catch (err) {
1318
1439
  if (options.output === "json") printJsonError(err);
1319
- console.error(chalk9.red(err instanceof Error ? err.message : "Failed to create test"));
1440
+ console.error(chalk10.red(err instanceof Error ? err.message : "Failed to create test"));
1320
1441
  process.exit(
1321
1442
  err instanceof ApiStatusError && err.status === 409 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR
1322
1443
  );
@@ -1400,15 +1521,15 @@ async function logout() {
1400
1521
  }
1401
1522
 
1402
1523
  // src/commands/dev-status.ts
1403
- import chalk10 from "chalk";
1524
+ import chalk11 from "chalk";
1404
1525
  function buildLine(info) {
1405
1526
  if (!info || typeof info.app_build_id !== "number") return null;
1406
1527
  const version = info.app_version ? ` version ${info.app_version}${info.app_build_number ? ` (${info.app_build_number})` : ""}` : "";
1407
1528
  return `${info.app_build_id}${version}`;
1408
1529
  }
1409
1530
  async function devStatus(opts) {
1410
- const state = readDevState(process.cwd());
1411
- if (!state) {
1531
+ const found = readDevStateFile(process.cwd());
1532
+ if (!found) {
1412
1533
  if (opts.json) {
1413
1534
  console.log(JSON.stringify({ running: false }));
1414
1535
  } else {
@@ -1419,6 +1540,11 @@ async function devStatus(opts) {
1419
1540
  }
1420
1541
  const { token, baseUrl } = resolveAuth(opts);
1421
1542
  const api = new ApiClient(baseUrl, token);
1543
+ if (!found.alive) {
1544
+ await reportOrphan(api, found.state, opts);
1545
+ return;
1546
+ }
1547
+ const state = found.state;
1422
1548
  let sessionStatus;
1423
1549
  let info;
1424
1550
  try {
@@ -1456,14 +1582,49 @@ async function devStatus(opts) {
1456
1582
  console.error(` url: ${state.page_url}`);
1457
1583
  console.error(` started: ${state.started_at}`);
1458
1584
  }
1459
- if (metro) console.error(chalk10.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
1585
+ if (metro) console.error(chalk11.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
1460
1586
  process.exit(0);
1461
1587
  }
1588
+ var LIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "queued", "starting", "active"]);
1589
+ async function reportOrphan(api, state, opts) {
1590
+ let status;
1591
+ try {
1592
+ status = (await api.getSession(state.session_id)).status;
1593
+ } catch {
1594
+ status = void 0;
1595
+ }
1596
+ const live = status !== void 0 && LIVE_STATUSES.has(status);
1597
+ if (!live && status !== void 0) clearDevState(process.cwd());
1598
+ if (opts.json) {
1599
+ console.log(
1600
+ JSON.stringify({
1601
+ running: false,
1602
+ pid: state.pid,
1603
+ session_id: state.session_id,
1604
+ session_status: status ?? null,
1605
+ orphaned: live
1606
+ })
1607
+ );
1608
+ } else if (live) {
1609
+ console.error(
1610
+ chalk11.yellow(
1611
+ `\u26A0 The dev process (pid ${state.pid}) has exited, but session ${state.session_id} is still ${status}.`
1612
+ )
1613
+ );
1614
+ console.error(` End it: vibeview stop ${state.session_id}`);
1615
+ } else {
1616
+ console.error(
1617
+ `No dev session running \u2014 the dev process (pid ${state.pid}) has exited` + (status ? ` and session ${state.session_id} is ${status}.` : ".")
1618
+ );
1619
+ }
1620
+ process.exit(1);
1621
+ }
1462
1622
 
1463
1623
  // src/commands/dev-stop.ts
1464
1624
  async function devStop(opts) {
1465
- const state = readDevState(process.cwd());
1466
- if (!state) {
1625
+ const found = readDevStateFile(process.cwd());
1626
+ const state = found?.state;
1627
+ if (!found || !state) {
1467
1628
  if (opts.json) {
1468
1629
  console.log(JSON.stringify({ stopped: false, reason: "no_dev_session" }));
1469
1630
  } else {
@@ -1476,10 +1637,12 @@ async function devStop(opts) {
1476
1637
  const api = new ApiClient(baseUrl, token);
1477
1638
  await api.stopSession(state.session_id).catch(() => {
1478
1639
  });
1479
- try {
1480
- process.kill(state.pid, "SIGTERM");
1481
- } catch (err) {
1482
- if (err.code !== "ESRCH") throw err;
1640
+ if (found.alive) {
1641
+ try {
1642
+ process.kill(state.pid, "SIGTERM");
1643
+ } catch (err) {
1644
+ if (err.code !== "ESRCH") throw err;
1645
+ }
1483
1646
  }
1484
1647
  clearDevState(process.cwd());
1485
1648
  killOwnedMetro(process.cwd());
@@ -1492,12 +1655,12 @@ async function devStop(opts) {
1492
1655
  }
1493
1656
 
1494
1657
  // src/commands/dev-reload.ts
1495
- import chalk11 from "chalk";
1658
+ import chalk12 from "chalk";
1496
1659
  function fail(opts, code, message, exit) {
1497
1660
  if (opts.json) {
1498
1661
  console.log(JSON.stringify({ reloaded: false, code, message }));
1499
1662
  } else {
1500
- console.error(chalk11.red(`error: ${message}`));
1663
+ console.error(chalk12.red(`error: ${message}`));
1501
1664
  }
1502
1665
  process.exit(exit);
1503
1666
  }
@@ -1550,7 +1713,7 @@ async function devReload(opts) {
1550
1713
  api,
1551
1714
  // Build output and the "reloaded in" line are human output: stderr, so
1552
1715
  // stdout stays reserved for the --json result.
1553
- log: (line) => console.error(chalk11.dim(line))
1716
+ log: (line) => console.error(chalk12.dim(line))
1554
1717
  });
1555
1718
  const started = Date.now();
1556
1719
  try {
@@ -1571,10 +1734,10 @@ async function devReload(opts) {
1571
1734
  }
1572
1735
 
1573
1736
  // src/commands/builds.ts
1574
- import chalk13 from "chalk";
1737
+ import chalk14 from "chalk";
1575
1738
 
1576
1739
  // src/lib/formatters/builds.ts
1577
- import chalk12 from "chalk";
1740
+ import chalk13 from "chalk";
1578
1741
  var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
1579
1742
  var UNBILLED_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
1580
1743
  function formatCostCell(costMicro, status) {
@@ -1582,13 +1745,13 @@ function formatCostCell(costMicro, status) {
1582
1745
  return `$${(Math.max(costMicro, 0) / 1e6).toFixed(2)}`;
1583
1746
  }
1584
1747
  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
1748
+ succeeded: chalk13.green,
1749
+ failed: chalk13.red,
1750
+ timed_out: chalk13.red,
1751
+ infra_failed: chalk13.red,
1752
+ canceled: chalk13.dim,
1753
+ running: chalk13.yellow,
1754
+ queued: chalk13.dim
1592
1755
  };
1593
1756
  function formatBuildsHuman(builds2) {
1594
1757
  if (builds2.length === 0) {
@@ -1605,7 +1768,7 @@ function formatBuildsHuman(builds2) {
1605
1768
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1606
1769
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1607
1770
  const join4 = (cells) => cells.join(" ").trimEnd();
1608
- const lines = [chalk12.bold(join4(padCells(HEADERS)))];
1771
+ const lines = [chalk13.bold(join4(padCells(HEADERS)))];
1609
1772
  for (const row of rows) {
1610
1773
  const padded = padCells(row);
1611
1774
  const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
@@ -1664,7 +1827,7 @@ async function buildsLogs(id, opts, deps) {
1664
1827
  console.log(JSON.stringify(logs));
1665
1828
  } else {
1666
1829
  if (logs.content) process.stdout.write(logs.content);
1667
- console.error(chalk13.dim(`status: ${logs.status}`));
1830
+ console.error(chalk14.dim(`status: ${logs.status}`));
1668
1831
  }
1669
1832
  process.exitCode = 0;
1670
1833
  return;
@@ -1761,7 +1924,7 @@ async function buildsDownload(id, opts, deps) {
1761
1924
  if (opts.json) {
1762
1925
  console.log(JSON.stringify(result));
1763
1926
  } else {
1764
- console.log(chalk13.green(`\u2713 saved ${result.path}`));
1927
+ console.log(chalk14.green(`\u2713 saved ${result.path}`));
1765
1928
  }
1766
1929
  process.exitCode = 0;
1767
1930
  } catch (err) {
@@ -1778,7 +1941,7 @@ async function buildsCancel(id, opts, deps) {
1778
1941
  if (opts.json) {
1779
1942
  console.log(JSON.stringify(build));
1780
1943
  } else {
1781
- console.log(`${chalk13.cyan(build.id)} ${build.status}`);
1944
+ console.log(`${chalk14.cyan(build.id)} ${build.status}`);
1782
1945
  }
1783
1946
  process.exitCode = 0;
1784
1947
  } catch (err) {
@@ -1795,7 +1958,7 @@ async function buildsDeleteArtifact(id, opts, deps) {
1795
1958
  if (opts.json) {
1796
1959
  console.log(JSON.stringify(result));
1797
1960
  } else {
1798
- console.log(chalk13.green(`\u2713 deleted artifact for ${id}`));
1961
+ console.log(chalk14.green(`\u2713 deleted artifact for ${id}`));
1799
1962
  }
1800
1963
  process.exitCode = 0;
1801
1964
  } catch (err) {
@@ -1806,11 +1969,11 @@ async function buildsDeleteArtifact(id, opts, deps) {
1806
1969
  function printError(opts, err) {
1807
1970
  const message = err instanceof Error ? err.message : String(err);
1808
1971
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1809
- console.error(chalk13.red(`error: ${message}`));
1972
+ console.error(chalk14.red(`error: ${message}`));
1810
1973
  }
1811
1974
 
1812
1975
  // src/commands/submit.ts
1813
- import chalk14 from "chalk";
1976
+ import chalk15 from "chalk";
1814
1977
  function defaultDeps2(client) {
1815
1978
  return {
1816
1979
  createSubmission: client.createSubmission.bind(client),
@@ -1834,7 +1997,7 @@ function submitConfigFor(cwd, platform) {
1834
1997
  cfg = loadDevConfig(cwd);
1835
1998
  } catch (err) {
1836
1999
  console.error(
1837
- chalk14.yellow(
2000
+ chalk15.yellow(
1838
2001
  `warning: vibeview.json could not be read \u2014 ignoring submit defaults (${err instanceof Error ? err.message : String(err)})`
1839
2002
  )
1840
2003
  );
@@ -1982,12 +2145,12 @@ async function submitCommand(opts, deps) {
1982
2145
  return;
1983
2146
  }
1984
2147
  if (!opts.json) {
1985
- console.error(` ${chalk14.cyan(sub.id)} ${chalk14.dim(`[${sub.platform}]`)}`);
2148
+ console.error(` ${chalk15.cyan(sub.id)} ${chalk15.dim(`[${sub.platform}]`)}`);
1986
2149
  }
1987
2150
  const final = await followSubmission(sub, opts, d);
1988
2151
  if (!opts.json && final.status === "succeeded" && (final.release_status ?? releaseStatus) === "draft") {
1989
2152
  console.error(
1990
- chalk14.dim("release status: draft \u2014 activate it from the Play Console when ready")
2153
+ chalk15.dim("release status: draft \u2014 activate it from the Play Console when ready")
1991
2154
  );
1992
2155
  }
1993
2156
  process.exitCode = final.status === "succeeded" ? 0 : 1;
@@ -2018,11 +2181,11 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
2018
2181
  });
2019
2182
  emit(opts.json, { event: "preflight", ...result });
2020
2183
  if (!opts.json) {
2021
- const mark = result.ready ? chalk14.green("\u2713") : chalk14.red("\u2717");
2184
+ const mark = result.ready ? chalk15.green("\u2713") : chalk15.red("\u2717");
2022
2185
  console.error(`${mark} ${result.message}`);
2023
2186
  if (result.ready) {
2024
2187
  console.error(
2025
- chalk14.dim(
2188
+ chalk15.dim(
2026
2189
  ` next: vibeview build --cloud --production --platform ${platform}, then vibeview submit --platform ${platform} --latest`
2027
2190
  )
2028
2191
  );
@@ -2056,7 +2219,7 @@ async function linkedIdentifier(platform, opts, deps) {
2056
2219
  }
2057
2220
  if (!app.package_name) return void 0;
2058
2221
  if (!opts.json) {
2059
- console.error(chalk14.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2222
+ console.error(chalk15.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2060
2223
  }
2061
2224
  return app.package_name;
2062
2225
  }
@@ -2097,14 +2260,14 @@ async function followSubmission(created, opts, deps) {
2097
2260
  if (opts.json) {
2098
2261
  emit(opts.json, { event: "status", id: created.id, status: "queued" });
2099
2262
  } else {
2100
- console.error(chalk14.dim("\u23F3 queued \u2014 waiting for a submission slot"));
2263
+ console.error(chalk15.dim("\u23F3 queued \u2014 waiting for a submission slot"));
2101
2264
  }
2102
2265
  } else if (log.status === "running" && !printedRunning) {
2103
2266
  printedRunning = true;
2104
2267
  if (opts.json) {
2105
2268
  emit(opts.json, { event: "status", id: created.id, status: "running" });
2106
2269
  } else {
2107
- console.error(`${chalk14.cyan("\u25B6")} submitting`);
2270
+ console.error(`${chalk15.cyan("\u25B6")} submitting`);
2108
2271
  }
2109
2272
  }
2110
2273
  if (log.content) {
@@ -2146,7 +2309,7 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2146
2309
  });
2147
2310
  if (!opts.json) {
2148
2311
  const ok = final.status === "succeeded";
2149
- const mark = ok ? chalk14.green("\u2713") : chalk14.red("\u2717");
2312
+ const mark = ok ? chalk15.green("\u2713") : chalk15.red("\u2717");
2150
2313
  const duration = formatDuration(final.started_at, final.finished_at);
2151
2314
  let line = `${mark} ${final.status}`;
2152
2315
  if (duration) line += ` (${duration})`;
@@ -2158,11 +2321,11 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2158
2321
  function printError2(opts, err) {
2159
2322
  const message = err instanceof Error ? err.message : String(err);
2160
2323
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2161
- console.error(chalk14.red(`error: ${message}`));
2324
+ console.error(chalk15.red(`error: ${message}`));
2162
2325
  }
2163
2326
 
2164
2327
  // src/commands/versions.ts
2165
- import chalk15 from "chalk";
2328
+ import chalk16 from "chalk";
2166
2329
  function defaultDeps3(client) {
2167
2330
  return {
2168
2331
  listAppVersions: client.listAppVersions.bind(client),
@@ -2212,7 +2375,7 @@ async function versionsSet(buildNumber, opts, deps) {
2212
2375
  console.log(JSON.stringify(version));
2213
2376
  } else {
2214
2377
  console.log(
2215
- chalk15.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk15.dim(
2378
+ chalk16.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk16.dim(
2216
2379
  " \u2014 the next autoIncrement production build gets " + (version.build_number + 1)
2217
2380
  )
2218
2381
  );
@@ -2232,18 +2395,18 @@ function formatVersionsHuman(versions2) {
2232
2395
  const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2233
2396
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2234
2397
  const join4 = (cells) => cells.join(" ").trimEnd();
2235
- const lines = [chalk15.bold(join4(padCells(HEADERS2)))];
2398
+ const lines = [chalk16.bold(join4(padCells(HEADERS2)))];
2236
2399
  for (const row of rows) lines.push(join4(padCells(row)));
2237
2400
  return lines.join("\n") + "\n";
2238
2401
  }
2239
2402
  function printError3(opts, err) {
2240
2403
  const message = err instanceof Error ? err.message : String(err);
2241
2404
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2242
- console.error(chalk15.red(`error: ${message}`));
2405
+ console.error(chalk16.red(`error: ${message}`));
2243
2406
  }
2244
2407
 
2245
2408
  // src/commands/whoami.ts
2246
- import chalk16 from "chalk";
2409
+ import chalk17 from "chalk";
2247
2410
  function defaultDeps4(opts) {
2248
2411
  const { token, baseUrl } = resolveAuth(opts);
2249
2412
  const client = new ApiClient(baseUrl, token);
@@ -2255,6 +2418,15 @@ function effectiveRole(role, tokenRole) {
2255
2418
  if (!tokenRole) return role;
2256
2419
  return (RANK[tokenRole] ?? 3) < (RANK[role] ?? 3) ? tokenRole : role;
2257
2420
  }
2421
+ function describeExpiry(expiresAt, now = /* @__PURE__ */ new Date()) {
2422
+ if (!expiresAt) return "never";
2423
+ const when = new Date(expiresAt);
2424
+ if (Number.isNaN(when.getTime())) return expiresAt;
2425
+ const date = when.toISOString().slice(0, 10);
2426
+ const days = Math.ceil((when.getTime() - now.getTime()) / 864e5);
2427
+ if (days <= 0) return `${date} (expired)`;
2428
+ return `${date} (in ${days} day${days === 1 ? "" : "s"})`;
2429
+ }
2258
2430
  async function whoAmI(opts, deps) {
2259
2431
  const d = deps ?? defaultDeps4(opts);
2260
2432
  try {
@@ -2265,27 +2437,33 @@ async function whoAmI(opts, deps) {
2265
2437
  process.exitCode = 0;
2266
2438
  return;
2267
2439
  }
2268
- console.log(`${chalk16.bold("Logged in as")} ${me.email}`);
2440
+ console.log(`${chalk17.bold("Logged in as")} ${me.email}`);
2269
2441
  if (me.memberships.length === 1) {
2270
- console.log(`${chalk16.bold("Organization")} ${me.memberships[0].organization_name}`);
2442
+ console.log(`${chalk17.bold("Organization")} ${me.memberships[0].organization_name}`);
2271
2443
  } else if (me.memberships.length > 1) {
2272
- console.log(chalk16.bold("Organizations"));
2444
+ console.log(chalk17.bold("Organizations"));
2273
2445
  for (const m of me.memberships) {
2274
2446
  console.log(` ${m.organization_name} (${m.role})`);
2275
2447
  }
2276
2448
  }
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"}`);
2449
+ console.log(`${chalk17.bold("Member role")} ${me.role ?? "\u2014"}`);
2450
+ console.log(`${chalk17.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
2451
+ console.log(`${chalk17.bold("Effective role")} ${effective ?? "\u2014"}`);
2452
+ if (me.token_access) {
2453
+ console.log(
2454
+ `${chalk17.bold("Access")} ${me.token_access === "ci" ? "CI only (upload builds, run tests, read results)" : "Full access"}`
2455
+ );
2456
+ console.log(`${chalk17.bold("Expires")} ${describeExpiry(me.token_expires_at)}`);
2457
+ }
2280
2458
  if (me.is_superadmin) {
2281
- console.log(chalk16.dim("Superadmin (platform-wide; does not raise the org role above)"));
2459
+ console.log(chalk17.dim("Superadmin (platform-wide; does not raise the org role above)"));
2282
2460
  }
2283
- if (me.role === "admin" && me.token_role && me.token_role !== "admin") {
2461
+ if (me.role === "admin" && me.token_role && me.token_role !== "admin" && me.token_access !== "ci") {
2284
2462
  console.log(
2285
- chalk16.yellow(
2463
+ chalk17.yellow(
2286
2464
  `
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")}.`
2465
+ 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.
2466
+ Create an admin API token in the dashboard and use ${chalk17.bold("--token")} or ${chalk17.bold("VIBEVIEW_API_TOKEN")}.`
2289
2467
  )
2290
2468
  );
2291
2469
  }
@@ -2295,7 +2473,7 @@ Create an admin API token in the dashboard and use ${chalk16.bold("--token")} or
2295
2473
  if (opts.json) {
2296
2474
  console.log(JSON.stringify({ event: "error", message }));
2297
2475
  }
2298
- console.error(chalk16.red(`error: ${message}`));
2476
+ console.error(chalk17.red(`error: ${message}`));
2299
2477
  process.exitCode = errorExitCode(err);
2300
2478
  }
2301
2479
  }
@@ -2303,7 +2481,7 @@ Create an admin API token in the dashboard and use ${chalk16.bold("--token")} or
2303
2481
  // src/commands/env.ts
2304
2482
  import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
2305
2483
  import { basename } from "path";
2306
- import chalk17 from "chalk";
2484
+ import chalk18 from "chalk";
2307
2485
  import { password } from "@inquirer/prompts";
2308
2486
 
2309
2487
  // src/lib/gitignore.ts
@@ -2359,7 +2537,7 @@ function defaultDeps5(client) {
2359
2537
  isIgnored: (path) => isGitIgnored(path),
2360
2538
  writeFile: writeSecureFile,
2361
2539
  readFile: (path) => readFileAsync(path),
2362
- warn: (message) => console.error(chalk17.yellow(`warning: ${message}`))
2540
+ warn: (message) => console.error(chalk18.yellow(`warning: ${message}`))
2363
2541
  };
2364
2542
  }
2365
2543
  async function envList(opts, deps) {
@@ -2391,7 +2569,7 @@ function formatEnvVarsHuman(variables) {
2391
2569
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2392
2570
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2393
2571
  const join4 = (cells) => cells.join(" ").trimEnd();
2394
- const lines = [chalk17.bold(join4(padCells(HEADERS3)))];
2572
+ const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
2395
2573
  for (const row of rows) lines.push(join4(padCells(row)));
2396
2574
  return lines.join("\n") + "\n";
2397
2575
  }
@@ -2441,7 +2619,7 @@ function printSetResult(opts, result) {
2441
2619
  if (opts.json) {
2442
2620
  console.log(JSON.stringify(result));
2443
2621
  } else {
2444
- console.log(chalk17.green(`\u2713 set ${result.name} (${result.visibility})`));
2622
+ console.log(chalk18.green(`\u2713 set ${result.name} (${result.visibility})`));
2445
2623
  }
2446
2624
  }
2447
2625
  async function envRm(name, opts, deps) {
@@ -2451,7 +2629,7 @@ async function envRm(name, opts, deps) {
2451
2629
  if (opts.json) {
2452
2630
  console.log(JSON.stringify({ event: "deleted", name }));
2453
2631
  } else {
2454
- console.log(chalk17.green(`\u2713 removed ${name}`));
2632
+ console.log(chalk18.green(`\u2713 removed ${name}`));
2455
2633
  }
2456
2634
  process.exitCode = 0;
2457
2635
  } catch (err) {
@@ -2483,7 +2661,7 @@ async function envPull(opts, deps) {
2483
2661
  if (opts.json) {
2484
2662
  console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
2485
2663
  } else {
2486
- console.log(chalk17.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
2664
+ console.log(chalk18.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
2487
2665
  }
2488
2666
  process.exitCode = 0;
2489
2667
  } catch (err) {
@@ -2522,7 +2700,7 @@ function escapeEnvValue(value) {
2522
2700
  function printError4(opts, err) {
2523
2701
  const message = err instanceof Error ? err.message : String(err);
2524
2702
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2525
- console.error(chalk17.red(`error: ${message}`));
2703
+ console.error(chalk18.red(`error: ${message}`));
2526
2704
  }
2527
2705
 
2528
2706
  // src/commands/credentials.ts
@@ -2534,7 +2712,7 @@ import {
2534
2712
  } from "fs/promises";
2535
2713
  import { mkdirSync } from "fs";
2536
2714
  import { basename as basename2, extname, join } from "path";
2537
- import chalk18 from "chalk";
2715
+ import chalk19 from "chalk";
2538
2716
  import { input, password as password2 } from "@inquirer/prompts";
2539
2717
  function clientFor2(opts) {
2540
2718
  const { token, baseUrl } = resolveAuth(opts);
@@ -2607,7 +2785,7 @@ function defaultDeps6(client) {
2607
2785
  mkdir: (dir) => mkdirSync(dir, { recursive: true }),
2608
2786
  writeFile: (path, data, mode) => writeFileAsync(path, data, mode !== void 0 ? { mode } : void 0),
2609
2787
  chmod: (path, mode) => chmodAsync(path, mode),
2610
- warn: (message) => console.error(chalk18.yellow(`warning: ${message}`))
2788
+ warn: (message) => console.error(chalk19.yellow(`warning: ${message}`))
2611
2789
  };
2612
2790
  }
2613
2791
  var ANDROID_KIND = "android_keystore";
@@ -2676,7 +2854,7 @@ async function credentialsAdd(opts, deps) {
2676
2854
  if (opts.json) {
2677
2855
  console.log(JSON.stringify(result));
2678
2856
  } else {
2679
- console.log(chalk18.green(`\u2713 added ${result.name} (${result.platform})`));
2857
+ console.log(chalk19.green(`\u2713 added ${result.name} (${result.platform})`));
2680
2858
  }
2681
2859
  process.exitCode = 0;
2682
2860
  } catch (err) {
@@ -2734,20 +2912,20 @@ async function credentialsGenerate(opts, deps) {
2734
2912
  }
2735
2913
  if (opts.json) {
2736
2914
  } else {
2737
- console.log(chalk18.green(`\u2713 generated ${result.name} (android)`));
2915
+ console.log(chalk19.green(`\u2713 generated ${result.name} (android)`));
2738
2916
  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)}`);
2917
+ console.log(` Keystore saved to: ${chalk19.bold(savedTo)}`);
2918
+ console.log(` Key alias: ${chalk19.bold(result.key_alias)}`);
2919
+ console.log(` Store password: ${chalk19.bold(result.store_password)}`);
2920
+ console.log(` Key password: ${chalk19.bold(result.key_password)}`);
2743
2921
  console.log("");
2744
2922
  console.log(
2745
- chalk18.yellow(
2923
+ chalk19.yellow(
2746
2924
  " These are shown ONCE \u2014 VibeView cannot recover the keystore file or its passwords. Back them up somewhere safe."
2747
2925
  )
2748
2926
  );
2749
2927
  console.log(
2750
- chalk18.yellow(
2928
+ chalk19.yellow(
2751
2929
  " The credential is already uploaded and active for android and androidtv production builds."
2752
2930
  )
2753
2931
  );
@@ -2773,19 +2951,19 @@ async function generateAppleCredential(platform, opts, d) {
2773
2951
  if (opts.json) {
2774
2952
  console.log(JSON.stringify(result));
2775
2953
  } else {
2776
- console.log(chalk18.green(`\u2713 generated ${result.name} (${result.platform})`));
2954
+ console.log(chalk19.green(`\u2713 generated ${result.name} (${result.platform})`));
2777
2955
  console.log("");
2778
- console.log(` Scope: ${chalk18.bold(result.app_public_id ?? "org-wide")}`);
2956
+ console.log(` Scope: ${chalk19.bold(result.app_public_id ?? "org-wide")}`);
2779
2957
  const expires = formatExpires(result);
2780
2958
  if (expires) {
2781
- console.log(` Expires: ${chalk18.bold(expires)}`);
2959
+ console.log(` Expires: ${chalk19.bold(expires)}`);
2782
2960
  }
2783
2961
  console.log("");
2784
2962
  console.log(
2785
2963
  ` The certificate and provisioning profile are stored with VibeView and already active for ${result.platform} production builds.`
2786
2964
  );
2787
2965
  console.log(
2788
- ` Run ${chalk18.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
2966
+ ` Run ${chalk19.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
2789
2967
  );
2790
2968
  }
2791
2969
  process.exitCode = 0;
@@ -2851,17 +3029,17 @@ async function credentialsDownload(name, opts, deps) {
2851
3029
  await d.chmod(path, 384);
2852
3030
  }
2853
3031
  if (!opts.json) {
2854
- console.log(chalk18.green(`\u2713 downloaded ${material.name} (${material.platform})`));
3032
+ console.log(chalk19.green(`\u2713 downloaded ${material.name} (${material.platform})`));
2855
3033
  console.log("");
2856
- console.log(` Saved to: ${chalk18.bold(savedTo)}`);
3034
+ console.log(` Saved to: ${chalk19.bold(savedTo)}`);
2857
3035
  if (profileSavedTo !== null) {
2858
- console.log(` Profile: ${chalk18.bold(profileSavedTo)}`);
3036
+ console.log(` Profile: ${chalk19.bold(profileSavedTo)}`);
2859
3037
  }
2860
3038
  for (const [key, value] of Object.entries(material.passwords)) {
2861
- console.log(` ${key}: ${chalk18.bold(value)}`);
3039
+ console.log(` ${key}: ${chalk19.bold(value)}`);
2862
3040
  }
2863
3041
  console.log("");
2864
- console.log(chalk18.yellow(" This download is recorded in your organization's audit log."));
3042
+ console.log(chalk19.yellow(" This download is recorded in your organization's audit log."));
2865
3043
  }
2866
3044
  process.exitCode = 0;
2867
3045
  } catch (err) {
@@ -2892,13 +3070,13 @@ async function ascKeyAdd(opts, deps) {
2892
3070
  if (opts.json) {
2893
3071
  console.log(JSON.stringify(result));
2894
3072
  } else {
2895
- console.log(chalk18.green(`\u2713 added App Store Connect key ${result.name}`));
3073
+ console.log(chalk19.green(`\u2713 added App Store Connect key ${result.name}`));
2896
3074
  console.log("");
2897
- console.log(` Key ID: ${chalk18.bold(result.key_id)}`);
2898
- console.log(` Issuer ID: ${chalk18.bold(result.issuer_id)}`);
3075
+ console.log(` Key ID: ${chalk19.bold(result.key_id)}`);
3076
+ console.log(` Issuer ID: ${chalk19.bold(result.issuer_id)}`);
2899
3077
  console.log("");
2900
3078
  console.log(
2901
- chalk18.yellow(
3079
+ chalk19.yellow(
2902
3080
  " The .p8 is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2903
3081
  )
2904
3082
  );
@@ -2933,7 +3111,7 @@ function formatAscKeysHuman(keys) {
2933
3111
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2934
3112
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2935
3113
  const join4 = (cells) => cells.join(" ").trimEnd();
2936
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
3114
+ const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
2937
3115
  for (const row of rows) lines.push(join4(padCells(row)));
2938
3116
  return lines.join("\n") + "\n";
2939
3117
  }
@@ -2944,7 +3122,7 @@ async function ascKeyRm(name, opts, deps) {
2944
3122
  if (opts.json) {
2945
3123
  console.log(JSON.stringify({ event: "deleted", name }));
2946
3124
  } else {
2947
- console.log(chalk18.green(`\u2713 removed App Store Connect key ${name}`));
3125
+ console.log(chalk19.green(`\u2713 removed App Store Connect key ${name}`));
2948
3126
  }
2949
3127
  process.exitCode = 0;
2950
3128
  } catch (err) {
@@ -2969,12 +3147,12 @@ async function playKeyAdd(opts, deps) {
2969
3147
  if (opts.json) {
2970
3148
  console.log(JSON.stringify(result));
2971
3149
  } else {
2972
- console.log(chalk18.green(`\u2713 added Google Play key ${result.name}`));
3150
+ console.log(chalk19.green(`\u2713 added Google Play key ${result.name}`));
2973
3151
  console.log("");
2974
- console.log(` Service account: ${chalk18.bold(result.client_email)}`);
3152
+ console.log(` Service account: ${chalk19.bold(result.client_email)}`);
2975
3153
  console.log("");
2976
3154
  console.log(
2977
- chalk18.yellow(
3155
+ chalk19.yellow(
2978
3156
  " The JSON key is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2979
3157
  )
2980
3158
  );
@@ -3009,7 +3187,7 @@ function formatPlayKeysHuman(keys) {
3009
3187
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3010
3188
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3011
3189
  const join4 = (cells) => cells.join(" ").trimEnd();
3012
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
3190
+ const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3013
3191
  for (const row of rows) lines.push(join4(padCells(row)));
3014
3192
  return lines.join("\n") + "\n";
3015
3193
  }
@@ -3020,7 +3198,7 @@ async function playKeyRm(name, opts, deps) {
3020
3198
  if (opts.json) {
3021
3199
  console.log(JSON.stringify({ event: "deleted", name }));
3022
3200
  } else {
3023
- console.log(chalk18.green(`\u2713 removed Google Play key ${name}`));
3201
+ console.log(chalk19.green(`\u2713 removed Google Play key ${name}`));
3024
3202
  }
3025
3203
  process.exitCode = 0;
3026
3204
  } catch (err) {
@@ -3089,7 +3267,7 @@ function formatCredentialsHuman(credentials2) {
3089
3267
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3090
3268
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3091
3269
  const join4 = (cells) => cells.join(" ").trimEnd();
3092
- const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
3270
+ const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3093
3271
  for (const row of rows) lines.push(join4(padCells(row)));
3094
3272
  return lines.join("\n") + "\n";
3095
3273
  }
@@ -3100,7 +3278,7 @@ async function credentialsRm(name, opts, deps) {
3100
3278
  if (opts.json) {
3101
3279
  console.log(JSON.stringify({ event: "deleted", name }));
3102
3280
  } else {
3103
- console.log(chalk18.green(`\u2713 removed ${name}`));
3281
+ console.log(chalk19.green(`\u2713 removed ${name}`));
3104
3282
  }
3105
3283
  process.exitCode = 0;
3106
3284
  } catch (err) {
@@ -3112,16 +3290,16 @@ function printKeystoreWriteFailure(opts, err, result) {
3112
3290
  const message = err instanceof Error ? err.message : String(err);
3113
3291
  if (opts.json) {
3114
3292
  console.log(JSON.stringify({ event: "error", message }));
3115
- console.error(chalk18.red(`error: ${message}`));
3293
+ console.error(chalk19.red(`error: ${message}`));
3116
3294
  return;
3117
3295
  }
3118
- console.error(chalk18.red(`error: ${message}`));
3296
+ console.error(chalk19.red(`error: ${message}`));
3119
3297
  console.error(
3120
- chalk18.red(
3298
+ chalk19.red(
3121
3299
  `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
3300
  )
3123
3301
  );
3124
- console.error(chalk18.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
3302
+ console.error(chalk19.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
3125
3303
  console.error("");
3126
3304
  console.error(` Key alias: ${result.key_alias}`);
3127
3305
  console.error(` Store password: ${result.store_password}`);
@@ -3135,11 +3313,11 @@ function printKeystoreWriteFailure(opts, err, result) {
3135
3313
  function printError5(opts, err) {
3136
3314
  const message = err instanceof Error ? err.message : String(err);
3137
3315
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3138
- console.error(chalk18.red(`error: ${message}`));
3316
+ console.error(chalk19.red(`error: ${message}`));
3139
3317
  }
3140
3318
 
3141
3319
  // src/commands/webhooks.ts
3142
- import chalk19 from "chalk";
3320
+ import chalk20 from "chalk";
3143
3321
  function clientFor3(opts) {
3144
3322
  const { token, baseUrl } = resolveAuth(opts);
3145
3323
  return new ApiClient(baseUrl, token);
@@ -3170,14 +3348,14 @@ async function webhooksAdd(opts, deps) {
3170
3348
  if (opts.json) {
3171
3349
  console.log(JSON.stringify(result));
3172
3350
  } else {
3173
- console.log(chalk19.green(`\u2713 added ${result.id} ${result.url}`));
3351
+ console.log(chalk20.green(`\u2713 added ${result.id} ${result.url}`));
3174
3352
  console.log("");
3175
- console.log(` Signing secret: ${chalk19.bold(result.secret)}`);
3353
+ console.log(` Signing secret: ${chalk20.bold(result.secret)}`);
3176
3354
  if (opts.secret !== void 0) {
3177
- console.log(chalk19.yellow(" (This is the secret you provided.)"));
3355
+ console.log(chalk20.yellow(" (This is the secret you provided.)"));
3178
3356
  }
3179
3357
  console.log(
3180
- chalk19.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
3358
+ chalk20.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
3181
3359
  );
3182
3360
  }
3183
3361
  process.exitCode = 0;
@@ -3208,7 +3386,7 @@ function formatWebhooksHuman(webhooks2) {
3208
3386
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3209
3387
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3210
3388
  const join4 = (cells) => cells.join(" ").trimEnd();
3211
- const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
3389
+ const lines = [chalk20.bold(join4(padCells(HEADERS3)))];
3212
3390
  for (const row of rows) lines.push(join4(padCells(row)));
3213
3391
  return lines.join("\n") + "\n";
3214
3392
  }
@@ -3234,7 +3412,7 @@ async function webhooksRm(id, opts, deps) {
3234
3412
  if (opts.json) {
3235
3413
  console.log(JSON.stringify({ event: "deleted", id }));
3236
3414
  } else {
3237
- console.log(chalk19.green(`\u2713 removed ${id}`));
3415
+ console.log(chalk20.green(`\u2713 removed ${id}`));
3238
3416
  }
3239
3417
  process.exitCode = 0;
3240
3418
  } catch (err) {
@@ -3267,13 +3445,13 @@ async function webhooksTest(id, opts, deps) {
3267
3445
  if (opts.json) console.log(JSON.stringify(ld));
3268
3446
  if (ld.status === "succeeded") {
3269
3447
  if (!opts.json) {
3270
- console.log(chalk19.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3448
+ console.log(chalk20.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3271
3449
  }
3272
3450
  process.exitCode = 0;
3273
3451
  } else {
3274
3452
  if (!opts.json) {
3275
3453
  const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
3276
- console.error(chalk19.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3454
+ console.error(chalk20.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3277
3455
  }
3278
3456
  process.exitCode = 1;
3279
3457
  }
@@ -3282,7 +3460,7 @@ async function webhooksTest(id, opts, deps) {
3282
3460
  }
3283
3461
  if (opts.json) console.log(JSON.stringify({ status: "pending" }));
3284
3462
  console.error(
3285
- chalk19.yellow(
3463
+ chalk20.yellow(
3286
3464
  `Delivery still pending after ${TEST_POLL_BUDGET_MS / 1e3}s \u2014 check \`vibeview webhooks list\` later for the result.`
3287
3465
  )
3288
3466
  );
@@ -3295,7 +3473,7 @@ async function webhooksTest(id, opts, deps) {
3295
3473
  function printError6(opts, err) {
3296
3474
  const message = err instanceof Error ? err.message : String(err);
3297
3475
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3298
- console.error(chalk19.red(`error: ${message}`));
3476
+ console.error(chalk20.red(`error: ${message}`));
3299
3477
  }
3300
3478
 
3301
3479
  // src/commands/list-devices.ts
@@ -3393,7 +3571,7 @@ import { dirname, join as join2 } from "path";
3393
3571
  import { fileURLToPath } from "url";
3394
3572
  import { spawnSync } from "child_process";
3395
3573
  import { confirm } from "@inquirer/prompts";
3396
- import chalk20 from "chalk";
3574
+ import chalk21 from "chalk";
3397
3575
  function resolveShippedSkillPath(fromDir) {
3398
3576
  for (const rel of ["..", join2("..", "..")]) {
3399
3577
  const candidate = join2(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
@@ -3421,23 +3599,23 @@ function registerMcp() {
3421
3599
  encoding: "utf8"
3422
3600
  });
3423
3601
  if (res.status === 0) {
3424
- console.log(chalk20.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
3602
+ console.log(chalk21.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
3425
3603
  return;
3426
3604
  }
3427
3605
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
3428
3606
  if (out.includes("already exists")) {
3429
- console.log(chalk20.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
3607
+ console.log(chalk21.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
3430
3608
  return;
3431
3609
  }
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"));
3610
+ console.error(chalk21.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
3611
+ console.error(chalk21.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
3434
3612
  }
3435
3613
  async function agentSetup(opts) {
3436
3614
  const moduleDir = dirname(fileURLToPath(import.meta.url));
3437
3615
  const shipped = resolveShippedSkillPath(moduleDir);
3438
3616
  if (!shipped) {
3439
3617
  console.error(
3440
- chalk20.red(
3618
+ chalk21.red(
3441
3619
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
3442
3620
  )
3443
3621
  );
@@ -3446,9 +3624,9 @@ async function agentSetup(opts) {
3446
3624
  }
3447
3625
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
3448
3626
  const result = installSkill(shipped, targetDir);
3449
- console.log(chalk20.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
3627
+ console.log(chalk21.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
3450
3628
  console.log(
3451
- chalk20.dim(
3629
+ chalk21.dim(
3452
3630
  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
3631
  )
3454
3632
  );
@@ -3456,7 +3634,7 @@ async function agentSetup(opts) {
3456
3634
  if (!claudeCliAvailable()) {
3457
3635
  if (opts.mcp === true) {
3458
3636
  console.error(
3459
- chalk20.yellow(
3637
+ chalk21.yellow(
3460
3638
  "\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
3461
3639
  )
3462
3640
  );
@@ -3470,7 +3648,7 @@ async function agentSetup(opts) {
3470
3648
  }
3471
3649
  console.log(
3472
3650
  `
3473
- Suggested line for your project's ${chalk20.bold("CLAUDE.md")} (or ${chalk20.bold(
3651
+ Suggested line for your project's ${chalk21.bold("CLAUDE.md")} (or ${chalk21.bold(
3474
3652
  "AGENTS.md"
3475
3653
  )} for other agents):
3476
3654
  `
@@ -3478,7 +3656,7 @@ Suggested line for your project's ${chalk20.bold("CLAUDE.md")} (or ${chalk20.bol
3478
3656
  console.log(` ${CLAUDE_MD_SUGGESTION}
3479
3657
  `);
3480
3658
  console.log(
3481
- chalk20.dim(
3659
+ chalk21.dim(
3482
3660
  "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
3661
  )
3484
3662
  );
@@ -3613,6 +3791,7 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
3613
3791
  }
3614
3792
  try {
3615
3793
  parseRunPlatform(opts.platform);
3794
+ refusePlatformWithApp(opts);
3616
3795
  } catch (err) {
3617
3796
  process.stderr.write(`error: ${err.message}
3618
3797
  `);
@@ -3681,6 +3860,7 @@ program.command("run-test <test-id>").description("Run a single test case").opti
3681
3860
  }
3682
3861
  try {
3683
3862
  parseRunPlatform(opts.platform);
3863
+ refusePlatformWithApp(opts);
3684
3864
  } catch (err) {
3685
3865
  process.stderr.write(`error: ${err.message}
3686
3866
  `);
@@ -3899,7 +4079,7 @@ program.command("dev-reload").description(
3899
4079
  program.command("mcp").description(
3900
4080
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
3901
4081
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
3902
- const { runMcpServer } = await import("./server-2JNYWF5L.js");
4082
+ const { runMcpServer } = await import("./server-FJSBWEHZ.js");
3903
4083
  await runMcpServer({ ...opts, version: pkg.version });
3904
4084
  });
3905
4085
  program.command("agent-setup").description(