vibeview 0.33.0 → 0.34.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.
@@ -384,6 +384,80 @@ function buildLabel(b) {
384
384
  if (b.note) parts.push(JSON.stringify(b.note.length > 60 ? `${b.note.slice(0, 57)}...` : b.note));
385
385
  return `${b.id} (${parts.join(", ")})`;
386
386
  }
387
+ function startedBuild(b) {
388
+ return {
389
+ id: b.id,
390
+ version: b.version ?? null,
391
+ build_number: b.build_number ?? null,
392
+ kind: buildKind(b),
393
+ uploaded_at: b.uploaded_at ?? null,
394
+ note: b.note ?? null
395
+ };
396
+ }
397
+ function otherDebugBuilds(app, installedId) {
398
+ const builds = debugBuildsNewestFirst(app);
399
+ const installed = app.builds.find((b) => b.id === installedId);
400
+ const seen = /* @__PURE__ */ new Set();
401
+ if (installed) seen.add(installed.version ?? "");
402
+ const out = [];
403
+ for (const b of builds) {
404
+ if (b.id === installedId) continue;
405
+ const key = b.version ?? "";
406
+ if (seen.has(key)) continue;
407
+ seen.add(key);
408
+ out.push(b);
409
+ }
410
+ return out;
411
+ }
412
+ function preferDevBuild(app, pinned, project) {
413
+ if (pinned !== void 0 || !app) return { buildId: pinned, reason: null };
414
+ const picked = pickDevBuild(app, project?.version ?? null);
415
+ return picked?.reason ? { buildId: picked.build.id, reason: picked.reason } : { buildId: void 0, reason: null };
416
+ }
417
+ function describeDevBuild(app, session, pick, project) {
418
+ const installedId = session?.app_build_id ?? pick.buildId ?? null;
419
+ const installed = app?.builds.find((b) => b.id === installedId);
420
+ const build = installed ? startedBuild(installed) : typeof installedId === "number" ? {
421
+ id: installedId,
422
+ version: session?.app_version ?? null,
423
+ build_number: session?.app_build_number ?? null,
424
+ kind: "unknown",
425
+ uploaded_at: null,
426
+ note: null
427
+ } : null;
428
+ return {
429
+ build,
430
+ reason: pick.reason,
431
+ pinned: pick.pinned,
432
+ other_debug_builds: app ? otherDebugBuilds(app, installedId).map(startedBuild) : [],
433
+ project
434
+ };
435
+ }
436
+ function buildLines(choice, opts) {
437
+ const lines = [];
438
+ if (!choice) return lines;
439
+ if (choice.build) lines.push(`build: ${buildLabel(choice.build)}`);
440
+ if (choice.reason) lines.push(`picked that build because ${choice.reason}`);
441
+ const project = choice.project;
442
+ if (!opts.standalone && !choice.pinned && !choice.reason && project && !project.version) {
443
+ const why = project.versionUnreadable ? ` (${project.versionUnreadable})` : "";
444
+ lines.push(
445
+ `couldn't read this project's app version${why}, so this is the newest debug build, not necessarily one of this version`
446
+ );
447
+ }
448
+ if (choice.other_debug_builds.length > 0) {
449
+ lines.push(
450
+ `other debug builds: ${choice.other_debug_builds.map((b) => buildLabel(b)).join("; ")} \u2014 pass ${opts.flag} to run one`
451
+ );
452
+ }
453
+ const rn = project?.reactNative;
454
+ if (!opts.standalone && rn) {
455
+ lines.push(
456
+ `this project: react-native ${rn}${project?.version ? `, version ${project.version}` : ""}. If the app shows a red error screen right after it starts (for example "React Native version mismatch", or "undefined is not a function" inside a native module), the build may have been made from other native code than this project \u2014 try a build of this project (${opts.flag}, see ${opts.listBuilds}).`
457
+ );
458
+ }
459
+ return lines;
460
+ }
387
461
 
388
462
  // src/commands/list-builds.ts
389
463
  import chalk from "chalk";
@@ -628,7 +702,18 @@ function describeLocalChanges(before, after) {
628
702
  return changes;
629
703
  }
630
704
 
705
+ // src/commands/build.ts
706
+ import { spawn as spawn2 } from "child_process";
707
+ import { createInterface } from "readline";
708
+ import { select as select2 } from "@inquirer/prompts";
709
+ import chalk3 from "chalk";
710
+
711
+ // src/lib/dev-setup.ts
712
+ import chalk2 from "chalk";
713
+ import { input, select } from "@inquirer/prompts";
714
+
631
715
  // src/lib/project-detect.ts
716
+ import { execFile } from "child_process";
632
717
  import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
633
718
  import { join as join4 } from "path";
634
719
  import ignore from "ignore";
@@ -828,16 +913,64 @@ function pbxprojText(cwd) {
828
913
  return null;
829
914
  }
830
915
  }
831
- function localAppIdentity(cwd, platform) {
916
+ var readExpoConfig = (cwd) => {
917
+ const cli = join4(cwd, "node_modules", "expo", "bin", "cli");
918
+ if (!existsSync2(cli)) {
919
+ return Promise.reject(new Error("Expo is not installed in this project (node_modules/expo)"));
920
+ }
921
+ return new Promise((resolve, reject) => {
922
+ execFile(
923
+ process.execPath,
924
+ [cli, "config", "--json", "--type", "public"],
925
+ {
926
+ cwd,
927
+ timeout: 3e4,
928
+ maxBuffer: 8 * 1024 * 1024,
929
+ env: { ...process.env, EXPO_NO_TELEMETRY: "1", CI: "1" }
930
+ },
931
+ (err, stdout, stderr) => {
932
+ if (err) {
933
+ const why = String(stderr || err.message).split("\n").map((l) => l.trim()).find((l) => l.length > 0);
934
+ reject(new Error(`expo config failed${why ? `: ${why}` : ""}`));
935
+ return;
936
+ }
937
+ try {
938
+ const parsed = JSON.parse(stdout);
939
+ if (parsed && typeof parsed === "object") resolve(parsed);
940
+ else reject(new Error("expo config printed no config"));
941
+ } catch {
942
+ reject(new Error("expo config printed something that is not JSON"));
943
+ }
944
+ }
945
+ );
946
+ });
947
+ };
948
+ async function localAppIdentity(cwd, platform, readConfig = readExpoConfig) {
832
949
  const rn = readJson(cwd, "node_modules/react-native/package.json");
833
950
  const reactNative = typeof rn?.version === "string" ? rn.version : null;
834
951
  const android = platform === "android" || platform === "androidtv";
835
952
  const ios = platform === "ios" || platform === "tvos";
836
953
  let version = null;
837
954
  let bundleId = null;
838
- const dynamic = existsSync2(join4(cwd, "app.config.js")) || existsSync2(join4(cwd, "app.config.ts"));
839
- const raw = dynamic ? null : readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
840
- const expo = raw ? "expo" in raw ? raw.expo : raw : null;
955
+ let versionUnreadable = null;
956
+ const dynamicName = ["app.config.ts", "app.config.js", "app.config.mjs", "app.config.cjs"].find(
957
+ (f) => existsSync2(join4(cwd, f))
958
+ );
959
+ const staticRaw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
960
+ const isExpo = Boolean(dynamicName) || staticRaw !== null && "expo" in staticRaw || existsSync2(join4(cwd, "node_modules", "expo", "package.json"));
961
+ let expo = null;
962
+ if (isExpo && (dynamicName || existsSync2(join4(cwd, "node_modules", "expo", "bin", "cli")))) {
963
+ try {
964
+ expo = await readConfig(cwd);
965
+ } catch (err) {
966
+ if (dynamicName) {
967
+ versionUnreadable = `${dynamicName} is dynamic and could not be evaluated: ${err instanceof Error ? err.message : String(err)}`;
968
+ }
969
+ }
970
+ }
971
+ if (expo === null && !dynamicName && staticRaw) {
972
+ expo = "expo" in staticRaw ? staticRaw.expo : staticRaw;
973
+ }
841
974
  if (expo && typeof expo === "object") {
842
975
  const cfg = expo;
843
976
  if (typeof cfg.version === "string") version = cfg.version;
@@ -859,18 +992,10 @@ function localAppIdentity(cwd, platform) {
859
992
  bundleId ??= uniqueMatch(pbx, /PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);/g);
860
993
  }
861
994
  }
862
- return { reactNative, version, bundleId };
995
+ return { reactNative, version, bundleId, versionUnreadable: version ? null : versionUnreadable };
863
996
  }
864
997
 
865
- // src/commands/build.ts
866
- import { spawn as spawn2 } from "child_process";
867
- import { createInterface } from "readline";
868
- import { select as select2 } from "@inquirer/prompts";
869
- import chalk3 from "chalk";
870
-
871
998
  // src/lib/dev-setup.ts
872
- import chalk2 from "chalk";
873
- import { input, select } from "@inquirer/prompts";
874
999
  function platformChoices(cwd) {
875
1000
  const choices = [
876
1001
  { value: "ios", name: "iOS simulator" },
@@ -968,7 +1093,7 @@ async function promptMetroCommand(cwd, platform) {
968
1093
  }
969
1094
 
970
1095
  // src/lib/source-pack.ts
971
- import { execFile, spawn } from "child_process";
1096
+ import { execFile as execFile2, spawn } from "child_process";
972
1097
  import { lstatSync, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
973
1098
  import { tmpdir } from "os";
974
1099
  import { join as join5 } from "path";
@@ -1107,7 +1232,7 @@ function probeTarNullSupport() {
1107
1232
  const listPath = join5(tmp, "empty.list");
1108
1233
  writeFileSync4(listPath, "");
1109
1234
  const out = join5(tmp, "probe.tgz");
1110
- execFile("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
1235
+ execFile2("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
1111
1236
  rmSync4(tmp, { recursive: true, force: true });
1112
1237
  resolve(!err);
1113
1238
  });
@@ -2448,6 +2573,13 @@ function queuedLine(position, reasonMessage) {
2448
2573
  const why = reasonMessage ?? "Every device is busy.";
2449
2574
  return `\u23F3 ${why} Waiting in the queue${where}, Ctrl-C to give up.`;
2450
2575
  }
2576
+ function devBuildLines(choice, standalone) {
2577
+ return buildLines(choice, {
2578
+ standalone,
2579
+ flag: "--build <id>",
2580
+ listBuilds: "vibeview list-builds"
2581
+ });
2582
+ }
2451
2583
  function emit(json, evt) {
2452
2584
  if (json) console.log(JSON.stringify(evt));
2453
2585
  }
@@ -2456,8 +2588,30 @@ async function startDevSession(cfg, onEvent = () => {
2456
2588
  const api = new ApiClient(cfg.baseUrl, cfg.token);
2457
2589
  const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone;
2458
2590
  const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model) : null;
2591
+ let app = null;
2592
+ let project = null;
2593
+ let pick = {
2594
+ buildId: cfg.buildId,
2595
+ reason: null
2596
+ };
2597
+ if (usesTunnel && cfg.projectDir) {
2598
+ try {
2599
+ app = cfg.app ?? await api.getApp(cfg.appId);
2600
+ } catch {
2601
+ app = null;
2602
+ }
2603
+ try {
2604
+ project = await localAppIdentity(cfg.projectDir, cfg.platform);
2605
+ } catch {
2606
+ project = null;
2607
+ }
2608
+ pick = preferDevBuild(app, cfg.buildId, project);
2609
+ }
2610
+ const body = { ...cfg, buildId: pick.buildId };
2459
2611
  const created = await api.createSession(
2460
- devSessionCreateBody(pool ? { ...cfg, deviceCategory: pool.category, model: pool.model } : cfg)
2612
+ devSessionCreateBody(
2613
+ pool ? { ...body, deviceCategory: pool.category, model: pool.model } : body
2614
+ )
2461
2615
  );
2462
2616
  const sessionId = created.session_id;
2463
2617
  let stopping = false;
@@ -2573,7 +2727,8 @@ async function startDevSession(cfg, onEvent = () => {
2573
2727
  stop: () => stopSession(0),
2574
2728
  reportedWarnings,
2575
2729
  session: active,
2576
- lapsedReason: () => leaseLapsedReason(Date.now() - lastLeaseAt)
2730
+ lapsedReason: () => leaseLapsedReason(Date.now() - lastLeaseAt),
2731
+ build: usesTunnel && cfg.projectDir ? describeDevBuild(app, active, { ...pick, pinned: cfg.buildId !== void 0 }, project) : null
2577
2732
  };
2578
2733
  } catch (err) {
2579
2734
  await stopSession(1);
@@ -3192,6 +3347,7 @@ async function runDevInProcess(opts) {
3192
3347
  deviceCategory: cfg.deviceCategory,
3193
3348
  model: cfg.model,
3194
3349
  buildId: pinnedBuildId,
3350
+ projectDir: process.cwd(),
3195
3351
  ...standalone ? { standalone } : {},
3196
3352
  metroPort: cfg.metroPort ?? 8081,
3197
3353
  // startDevSession's own browser-open happens before we've written the
@@ -3249,6 +3405,8 @@ async function runDevInProcess(opts) {
3249
3405
  }
3250
3406
  );
3251
3407
  const keysLine = rokuBuild ? `Press ${chalk4.bold("r")} to reload the channel, ${chalk4.bold("q")} or Ctrl-C to end the session.` : "Ctrl-C to end the session.";
3408
+ for (const line of devBuildLines(handle.build, standalone))
3409
+ console.error(chalk4.dim(` ${line}`));
3252
3410
  console.error(`
3253
3411
  Watch and interact: ${chalk4.cyan(handle.pageUrl)}
3254
3412
  ${keysLine}
@@ -3266,7 +3424,8 @@ async function runDevInProcess(opts) {
3266
3424
  event: "session_ready",
3267
3425
  session_id: handle.sessionId,
3268
3426
  url: handle.pageUrl,
3269
- pid: process.pid
3427
+ pid: process.pid,
3428
+ build: handle.build ?? null
3270
3429
  });
3271
3430
  if (opts.open !== false) await openBrowser(handle.pageUrl).catch(() => {
3272
3431
  });
@@ -3398,6 +3557,8 @@ async function dev(opts) {
3398
3557
  } else {
3399
3558
  console.error(chalk4.dim(`Using ${resolveAuth(opts).baseUrl}`));
3400
3559
  console.error(chalk4.green(`\u2713 dev session started in the background (pid ${parsed.pid})`));
3560
+ for (const line2 of devBuildLines(parsed.build, false))
3561
+ console.error(chalk4.dim(` ${line2}`));
3401
3562
  console.error(`
3402
3563
  Watch and interact: ${chalk4.cyan(parsed.url)}`);
3403
3564
  console.error(chalk4.dim(` vibeview dev-status \u2014 check on it`));
@@ -4056,12 +4217,12 @@ var AGENT_COMMANDS = [
4056
4217
 
4057
4218
  // src/agent/render.ts
4058
4219
  import chalk6 from "chalk";
4059
- function renderEnvelope(env) {
4220
+ function renderEnvelope(env, label = env.tool) {
4060
4221
  const lines = [];
4061
4222
  if (env.ok) {
4062
- lines.push(chalk6.green(`ok`) + chalk6.dim(` (${env.tool})`));
4223
+ lines.push(chalk6.green(`ok`) + chalk6.dim(` (${label})`));
4063
4224
  } else {
4064
- lines.push(chalk6.red(`error`) + chalk6.dim(` (${env.tool})`));
4225
+ lines.push(chalk6.red(`error`) + chalk6.dim(` (${label})`));
4065
4226
  }
4066
4227
  lines.push(...resultLines(env.result));
4067
4228
  if (env.observation) {
@@ -4367,7 +4528,7 @@ async function runAgentVerb(cmd, positionals, opts) {
4367
4528
  JSON.stringify({ ...envelope, result: { written_to: outPath, bytes: imageBytes.length } })
4368
4529
  );
4369
4530
  } else {
4370
- process.stderr.write(renderEnvelope(envelope) + "\n");
4531
+ process.stderr.write(renderEnvelope(envelope, cmd.verb) + "\n");
4371
4532
  console.log(outPath);
4372
4533
  }
4373
4534
  process.exit(agentExitCode(envelope));
@@ -4376,7 +4537,7 @@ async function runAgentVerb(cmd, positionals, opts) {
4376
4537
  if (opts.json) {
4377
4538
  console.log(JSON.stringify(envelope));
4378
4539
  } else {
4379
- console.log(renderEnvelope(envelope));
4540
+ console.log(renderEnvelope(envelope, cmd.verb));
4380
4541
  if (cmd.verb === "press" && !envelope.ok) process.stderr.write(pressButtonsHelp() + "\n");
4381
4542
  }
4382
4543
  process.exit(agentExitCode(envelope));
@@ -4391,11 +4552,9 @@ export {
4391
4552
  loadDevConfig,
4392
4553
  resolveMetro,
4393
4554
  parseBuildId,
4394
- buildKind,
4395
4555
  checkDevBuild,
4396
- debugBuildsNewestFirst,
4397
- pickDevBuild,
4398
- buildLabel,
4556
+ describeDevBuild,
4557
+ buildLines,
4399
4558
  buildRows,
4400
4559
  formatBuildRowsHuman,
4401
4560
  listBuilds,
@@ -4407,7 +4566,6 @@ export {
4407
4566
  checkMetro,
4408
4567
  loadLocalDevConfig,
4409
4568
  effectiveDevConfig,
4410
- localAppIdentity,
4411
4569
  buildCommand,
4412
4570
  formatDuration,
4413
4571
  BUILD_TERMINAL_STATUSES,
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  readDevStateFile,
25
25
  readMetroState,
26
26
  runAgentVerb
27
- } from "./chunk-D3OZPFJ3.js";
27
+ } from "./chunk-TPZG7NCU.js";
28
28
  import {
29
29
  ApiClient,
30
30
  ApiStatusError,
@@ -60,10 +60,25 @@ import chalk2 from "chalk";
60
60
  // src/lib/formatters/human.ts
61
61
  import chalk from "chalk";
62
62
 
63
+ // src/lib/formatters/not-checked.ts
64
+ function notCheckedSteps(steps) {
65
+ return (steps ?? []).filter((s) => s.status === "not_checked");
66
+ }
67
+ function runStepsNotChecked(result) {
68
+ if (typeof result.steps_not_checked === "number") return result.steps_not_checked;
69
+ return notCheckedSteps(result.steps).length;
70
+ }
71
+ function stepsNotCheckedLabel(n) {
72
+ return `${n} step${n === 1 ? "" : "s"} not checked`;
73
+ }
74
+ function notCheckedSuffix(status, n) {
75
+ return status === "passed" && n && n > 0 ? ` (${stepsNotCheckedLabel(n)})` : "";
76
+ }
77
+
63
78
  // src/lib/formatters/cases.ts
64
79
  function resultCases(result) {
65
80
  if (Array.isArray(result.test_runs)) return result.test_runs;
66
- const failedStep = result.steps?.find((s) => s.status !== "passed");
81
+ const failedStep = result.steps?.find((s) => s.status === "failed" || s.status === "error");
67
82
  return [
68
83
  {
69
84
  case_name: result.case_name || result.run_id,
@@ -73,7 +88,12 @@ function resultCases(result) {
73
88
  error_kind: failedStep?.error_kind ?? null,
74
89
  error_kind_description: failedStep?.error_kind_description ?? null,
75
90
  screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
76
- url: result.url
91
+ url: result.url,
92
+ steps_not_checked: runStepsNotChecked(result),
93
+ not_checked_steps: notCheckedSteps(result.steps).map((s) => ({
94
+ step: s.step,
95
+ note: s.note ?? null
96
+ }))
77
97
  }
78
98
  ];
79
99
  }
@@ -100,11 +120,16 @@ function formatHuman(result, verbose) {
100
120
  const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
101
121
  lines.push("");
102
122
  const title = result.suite_name || result.case_name || result.run_id;
103
- lines.push(` ${icon} ${chalk.bold(title)} ${chalk.dim(duration)}`);
104
123
  const cases = resultCases(result);
124
+ const notChecked = cases.reduce((n, t) => n + (t.steps_not_checked ?? 0), 0);
125
+ const runSuffix = notCheckedSuffix(result.status, notChecked);
126
+ lines.push(
127
+ ` ${icon}${runSuffix ? chalk.yellow(runSuffix) : ""} ${chalk.bold(title)} ${chalk.dim(duration)}`
128
+ );
105
129
  const passed = cases.filter((t) => t.status === "passed").length;
106
130
  const total = cases.length;
107
- lines.push(` ${chalk.dim(`${passed}/${total} cases passed`)}`);
131
+ const notCheckedNote = notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
132
+ lines.push(` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}`)}`);
108
133
  if (result.url) {
109
134
  lines.push(` ${chalk.dim(result.url)}`);
110
135
  }
@@ -114,7 +139,10 @@ function formatHuman(result, verbose) {
114
139
  for (const tr of suiteCases) {
115
140
  const caseIcon = tr.status === "passed" ? chalk.green(" PASS") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
116
141
  const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
117
- lines.push(` ${caseIcon} ${tr.case_name} ${chalk.dim(caseDuration)}`);
142
+ const caseSuffix = notCheckedSuffix(tr.status, tr.steps_not_checked);
143
+ lines.push(
144
+ ` ${caseIcon}${caseSuffix ? chalk.yellow(caseSuffix) : ""} ${tr.case_name} ${chalk.dim(caseDuration)}`
145
+ );
118
146
  if (tr.url) {
119
147
  lines.push(` ${chalk.dim(tr.url)}`);
120
148
  }
@@ -129,7 +157,7 @@ function formatHuman(result, verbose) {
129
157
  lines.push(` ${chalk.bold(result.case_name)}`);
130
158
  }
131
159
  for (const step of result.steps) {
132
- const stepIcon = step.status === "passed" ? chalk.green(" PASS") : chalk.red(" FAIL");
160
+ const stepIcon = step.status === "passed" ? chalk.green(" PASS") : step.status === "not_checked" ? chalk.yellow(" NOT CHECKED") : chalk.red(" FAIL");
133
161
  const stepDuration = step.duration_ms ? `${(step.duration_ms / 1e3).toFixed(1)}s` : "-";
134
162
  lines.push(` ${stepIcon} ${step.step} ${chalk.dim(stepDuration)}`);
135
163
  if (verbose && step.actions && step.actions.length > 0) {
@@ -137,7 +165,9 @@ function formatHuman(result, verbose) {
137
165
  lines.push(` ${chalk.dim(formatAction(action))}`);
138
166
  }
139
167
  }
140
- if (step.status !== "passed") {
168
+ if (step.status === "not_checked") {
169
+ if (step.note) lines.push(` ${chalk.dim(step.note)}`);
170
+ } else if (step.status !== "passed") {
141
171
  lines.push(...failureLines(step));
142
172
  }
143
173
  }
@@ -156,13 +186,33 @@ function formatHuman(result, verbose) {
156
186
 
157
187
  // src/lib/formatters/json.ts
158
188
  function formatJson(result) {
159
- return JSON.stringify(result, null, 2);
189
+ const doc = Array.isArray(result.test_runs) ? result : { ...result, steps_not_checked: runStepsNotChecked(result) };
190
+ return JSON.stringify(doc, null, 2);
160
191
  }
161
192
 
162
193
  // src/lib/formatters/junit.ts
163
194
  function escapeXml(s) {
164
195
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
165
196
  }
197
+ function caseDetailLines(c) {
198
+ const n = c.status === "passed" ? c.steps_not_checked ?? 0 : 0;
199
+ const lines = [];
200
+ if (n > 0) {
201
+ lines.push(" <properties>");
202
+ lines.push(` <property name="steps_not_checked" value="${n}" />`);
203
+ lines.push(" </properties>");
204
+ }
205
+ const out = [];
206
+ if (c.url) out.push(c.url);
207
+ if (n > 0) {
208
+ out.push(`${stepsNotCheckedLabel(n)}:`);
209
+ for (const s of c.not_checked_steps ?? []) {
210
+ out.push(`- ${s.step}${s.note ? ` (${s.note})` : ""}`);
211
+ }
212
+ }
213
+ if (out.length) lines.push(` <system-out>${escapeXml(out.join("\n"))}</system-out>`);
214
+ return lines;
215
+ }
166
216
  function formatJunit(result) {
167
217
  const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
168
218
  const cases = resultCases(result);
@@ -194,8 +244,8 @@ function formatJunit(result) {
194
244
  for (const tr of cases) {
195
245
  const caseName = escapeXml(tr.case_name);
196
246
  const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
197
- const out = tr.url ? ` <system-out>${escapeXml(tr.url)}</system-out>` : null;
198
- if (tr.status === "passed" && !out) {
247
+ const detail = caseDetailLines(tr);
248
+ if (tr.status === "passed" && detail.length === 0) {
199
249
  lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
200
250
  continue;
201
251
  }
@@ -207,7 +257,7 @@ function formatJunit(result) {
207
257
  const msg = escapeXml(tr.error_message || "Infrastructure error");
208
258
  lines.push(` <error message="${msg}" type="InfraError">${msg}</error>`);
209
259
  }
210
- if (out) lines.push(out);
260
+ lines.push(...detail);
211
261
  lines.push(" </testcase>");
212
262
  }
213
263
  lines.push(" </testsuite>");
@@ -240,8 +290,12 @@ var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "canc
240
290
  function caseLines(c, classname) {
241
291
  const name = escapeXml(c.name);
242
292
  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} />`];
293
+ const detail = caseDetailLines({
294
+ status: c.verdict,
295
+ url: c.url,
296
+ steps_not_checked: c.steps_not_checked
297
+ });
298
+ if (c.verdict === "passed" && detail.length === 0) return [`${head} />`];
245
299
  const lines = [`${head}>`];
246
300
  const msg = escapeXml(c.message ?? "");
247
301
  if (c.verdict === "failure") {
@@ -251,7 +305,7 @@ function caseLines(c, classname) {
251
305
  } else if (c.verdict === "skipped") {
252
306
  lines.push(` <skipped message="${msg}" />`);
253
307
  }
254
- if (out) lines.push(out);
308
+ lines.push(...detail);
255
309
  lines.push(" </testcase>");
256
310
  return lines;
257
311
  }
@@ -263,7 +317,12 @@ function formatMultiSuiteJunit(result, opts = {}) {
263
317
  const cases = (sr.case_runs ?? []).map((c) => {
264
318
  const name = c.case_name || c.run_id;
265
319
  const time = c.duration_ms ? (c.duration_ms / 1e3).toFixed(1) : "0";
266
- const base = { name, time, url: c.url || void 0 };
320
+ const base = {
321
+ name,
322
+ time,
323
+ url: c.url || void 0,
324
+ steps_not_checked: c.steps_not_checked
325
+ };
267
326
  switch (c.status) {
268
327
  case "passed":
269
328
  return { ...base, verdict: "passed" };
@@ -1039,8 +1098,13 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
1039
1098
  } else if (opts.output === "junit") {
1040
1099
  process.stdout.write(formatMultiSuiteJunit(last) + "\n");
1041
1100
  } else {
1101
+ const totalNotChecked = last.suite_runs.reduce(
1102
+ (n, sr) => n + (sr.case_runs ?? []).reduce((m, cr) => m + (cr.steps_not_checked ?? 0), 0),
1103
+ 0
1104
+ );
1105
+ const aggregateNote = totalNotChecked > 0 ? chalk5.yellow(` (${stepsNotCheckedLabel(totalNotChecked)})`) : "";
1042
1106
  process.stdout.write(`
1043
- Aggregate: ${last.status}
1107
+ Aggregate: ${last.status}${aggregateNote}
1044
1108
  `);
1045
1109
  for (const sr of last.suite_runs) {
1046
1110
  const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
@@ -1056,8 +1120,9 @@ Aggregate: ${last.status}
1056
1120
  for (const cr of sr.case_runs ?? []) {
1057
1121
  const caseMark = cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
1058
1122
  const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
1123
+ const unchecked = notCheckedSuffix(cr.status, cr.steps_not_checked);
1059
1124
  process.stdout.write(
1060
- ` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${dur} ${chalk5.dim(cr.url)}
1125
+ ` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
1061
1126
  `
1062
1127
  );
1063
1128
  if (cr.error_message && cr.status !== "passed") {
@@ -4079,7 +4144,7 @@ program.command("dev-reload").description(
4079
4144
  program.command("mcp").description(
4080
4145
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4081
4146
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4082
- const { runMcpServer } = await import("./server-GHCQHKYX.js");
4147
+ const { runMcpServer } = await import("./server-RDGEVJD4.js");
4083
4148
  await runMcpServer({ ...opts, version: pkg.version });
4084
4149
  });
4085
4150
  program.command("agent-setup").description(
@@ -4,15 +4,14 @@ import {
4
4
  DEV_PLATFORMS,
5
5
  PLATFORM_DEVICE,
6
6
  PLATFORM_NEEDS_METRO,
7
- buildKind,
8
- buildLabel,
7
+ buildLines,
9
8
  buildRows,
10
9
  buildToolCall,
11
10
  checkDevBuild,
12
11
  checkMetro,
13
12
  createRokuPush,
14
- debugBuildsNewestFirst,
15
13
  decodeScreenshotResult,
14
+ describeDevBuild,
16
15
  effectiveDevConfig,
17
16
  filterDeviceModels,
18
17
  formatBuildRowsHuman,
@@ -20,15 +19,13 @@ import {
20
19
  isDevPlatform,
21
20
  loadDevConfig,
22
21
  loadLocalDevConfig,
23
- localAppIdentity,
24
22
  parseBuildId,
25
- pickDevBuild,
26
23
  platformWithArticle,
27
24
  readDevState,
28
25
  renderEnvelope,
29
26
  resolveMetro,
30
27
  startDevSession
31
- } from "./chunk-D3OZPFJ3.js";
28
+ } from "./chunk-TPZG7NCU.js";
32
29
  import {
33
30
  ApiClient,
34
31
  NotFoundError,
@@ -117,7 +114,7 @@ var DEV_START_DEF = {
117
114
  },
118
115
  build_id: {
119
116
  type: "number",
120
- description: 'Run this uploaded build of the app (an id from list_builds). Without standalone it must be a debug build (kind "debug"); with standalone any build of the app works. Omitted: the newest debug build, or with standalone the newest release build. Not for roku.'
117
+ description: `Run this uploaded build of the app (an id from list_builds). Without standalone it must be a debug build (kind "debug"); with standalone any build of the app works. Omitted: the newest debug build (preferring one of the local project's own app version over a newer one of another version), or with standalone the newest release build. Not for roku.`
121
118
  },
122
119
  standalone: {
123
120
  type: "boolean",
@@ -240,6 +237,9 @@ async function readSession(deps, sessionId) {
240
237
  function isEndedStatus(status) {
241
238
  return status === "stopped" || status === "failed";
242
239
  }
240
+ function capitalize(text) {
241
+ return text.charAt(0).toUpperCase() + text.slice(1);
242
+ }
243
243
  function endedLine(ended) {
244
244
  const why = ended.reason ? `: ${ended.reason}` : "";
245
245
  return `session ${ended.sessionId} has ended (${ended.status}${why})`;
@@ -329,7 +329,7 @@ async function handleToolCall(toolName, args, deps, extra = {}) {
329
329
  if (cmd.verb === "screenshot" && envelope.ok) {
330
330
  return screenshotToolResult(envelope, args);
331
331
  }
332
- return textResult(renderEnvelope(envelope), envelope, !envelope.ok);
332
+ return textResult(renderEnvelope(envelope, toolNameFor(cmd.verb)), envelope, !envelope.ok);
333
333
  }
334
334
  function screenshotToolResult(envelope, args) {
335
335
  const { image, preview } = splitScreenshotResult(envelope.result);
@@ -351,7 +351,7 @@ function screenshotToolResult(envelope, args) {
351
351
  ...envelope,
352
352
  result: { written_to: outPath, bytes: imageBytes.length }
353
353
  };
354
- const lines = [`${renderEnvelope(sanitized)}
354
+ const lines = [`${renderEnvelope(sanitized, "screenshot")}
355
355
  screenshot written to ${outPath}`];
356
356
  let inlineBlock = null;
357
357
  if (args.inline === true) {
@@ -457,7 +457,7 @@ async function handleDevStart(args, deps, extra = {}) {
457
457
  const lines = [`${standalone ? "session" : "dev session"} started: ${payload.session_id}`];
458
458
  const device = result.device ? deviceLine(result.device) : null;
459
459
  if (device) lines.push(device);
460
- lines.push(...buildLines(result, standalone));
460
+ lines.push(...buildLines2(result, standalone));
461
461
  if (typeof result.queuedSeconds === "number") {
462
462
  lines.push(`waited ${Math.round(result.queuedSeconds)} s in the queue for a device`);
463
463
  }
@@ -467,35 +467,17 @@ async function handleDevStart(args, deps, extra = {}) {
467
467
  return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
468
468
  }
469
469
  }
470
- function buildLines(result, standalone) {
471
- const lines = [];
472
- if (result.build) {
473
- lines.push(`build: ${buildLabel(result.build)}`);
474
- }
475
- if (result.buildReason) lines.push(`picked that build because ${result.buildReason}`);
476
- const others = result.otherBuilds ?? [];
477
- if (others.length > 0) {
478
- lines.push(
479
- `other debug builds: ${others.map((b) => buildLabel(b)).join("; ")} \u2014 pass build_id to run one`
480
- );
481
- }
482
- const rn = result.project?.reactNative;
483
- if (!standalone && rn) {
484
- lines.push(
485
- `this project: react-native ${rn}. If the app shows "React Native version mismatch", the build was made with another React Native: run a build made from this project (build_id, see list_builds).`
486
- );
487
- }
488
- return lines;
489
- }
490
- function startedBuild(b) {
491
- return {
492
- id: b.id,
493
- version: b.version ?? null,
494
- build_number: b.build_number ?? null,
495
- kind: buildKind(b),
496
- uploaded_at: b.uploaded_at ?? null,
497
- note: b.note ?? null
498
- };
470
+ function buildLines2(result, standalone) {
471
+ return buildLines(
472
+ {
473
+ build: result.build ?? null,
474
+ reason: result.buildReason ?? null,
475
+ pinned: result.pinned === true,
476
+ other_debug_builds: result.otherBuilds ?? [],
477
+ project: result.project ?? null
478
+ },
479
+ { standalone, flag: "build_id", listBuilds: "list_builds" }
480
+ );
499
481
  }
500
482
  function deviceLine(d) {
501
483
  const name = d.model || d.name;
@@ -631,7 +613,7 @@ async function handleDevStop(deps) {
631
613
  }
632
614
  if (!current) {
633
615
  return textResult(
634
- ended ? `no dev session was running \u2014 ${endedLine(ended)} already` : "no dev session was running",
616
+ ended ? `no dev session was running \u2014 it had already ended. ${capitalize(endedLine(ended))}` : "no dev session was running",
635
617
  { session_id: null, stopped: false }
636
618
  );
637
619
  }
@@ -823,22 +805,8 @@ function createDevSessionController(auth, webUrl) {
823
805
  if (buildId !== void 0) {
824
806
  checkDevBuild(app, buildId, cfg.platform, { standalone });
825
807
  }
826
- const project = usesMetro ? localAppIdentity(process.cwd(), cfg.platform) : null;
827
- let buildReason = null;
828
- if (buildId === void 0 && usesMetro) {
829
- const picked = pickDevBuild(app, project?.version ?? null);
830
- if (picked?.reason) {
831
- buildId = picked.build.id;
832
- buildReason = picked.reason;
833
- }
834
- }
835
808
  const isTv = PLATFORM_DEVICE[cfg.platform].tv;
836
809
  const warnings = [];
837
- if (project?.bundleId && app.package_name && project.bundleId !== app.package_name) {
838
- warnings.push(
839
- `this project's ${isTv || cfg.platform.startsWith("android") ? "package name" : "bundle id"} is ${project.bundleId}, but app ${app.public_id} is ${app.package_name} \u2014 check that dev_start runs the app this project builds (list_apps)`
840
- );
841
- }
842
810
  const progress = startProgress(cfg.onProgress);
843
811
  let started = null;
844
812
  try {
@@ -853,6 +821,11 @@ function createDevSessionController(auth, webUrl) {
853
821
  // A named model decides the category itself (startDevSession).
854
822
  model: cfg.model,
855
823
  buildId,
824
+ // The build choice (a debug build of this project's version over
825
+ // a newer one of another) and its report live in startDevSession,
826
+ // shared with `vibeview dev` and `--detach`.
827
+ app,
828
+ ...usesMetro ? { projectDir: process.cwd() } : {},
856
829
  ...standalone ? { standalone } : {},
857
830
  metroPort,
858
831
  open: false,
@@ -880,23 +853,26 @@ function createDevSessionController(auth, webUrl) {
880
853
  handle = started;
881
854
  platform = cfg.platform;
882
855
  ended = null;
883
- const installedId = handle.session?.app_build_id ?? buildId ?? null;
884
- const installed = app.builds.find((b) => b.id === installedId);
885
- const others = usesMetro ? debugBuildsNewestFirst(app).filter((b) => b.id !== installedId) : [];
856
+ const choice = handle.build ?? describeDevBuild(
857
+ app,
858
+ handle.session,
859
+ { buildId, reason: null, pinned: buildId !== void 0 },
860
+ null
861
+ );
862
+ const project = choice.project;
863
+ if (project?.bundleId && app.package_name && project.bundleId !== app.package_name) {
864
+ warnings.push(
865
+ `this project's ${isTv || cfg.platform.startsWith("android") ? "package name" : "bundle id"} is ${project.bundleId}, but app ${app.public_id} is ${app.package_name} \u2014 check that dev_start runs the app this project builds (list_apps)`
866
+ );
867
+ }
886
868
  return {
887
869
  sessionId: handle.sessionId,
888
870
  pageUrl: handle.pageUrl,
889
871
  device: startedDevice(handle.session),
890
- build: installed ? startedBuild(installed) : typeof installedId === "number" ? {
891
- id: installedId,
892
- version: handle.session?.app_version ?? null,
893
- build_number: handle.session?.app_build_number ?? null,
894
- kind: "unknown",
895
- uploaded_at: null,
896
- note: null
897
- } : null,
898
- buildReason,
899
- otherBuilds: others.slice(0, 3).map(startedBuild),
872
+ build: choice.build,
873
+ buildReason: choice.reason,
874
+ pinned: choice.pinned,
875
+ otherBuilds: usesMetro ? choice.other_debug_builds : [],
900
876
  project,
901
877
  queuedSeconds: progress.queuedSeconds(),
902
878
  warnings
@@ -1117,7 +1093,7 @@ export {
1117
1093
  MCP_INSTRUCTIONS,
1118
1094
  SHUTDOWN_TIMEOUT_MS,
1119
1095
  START_PROGRESS_INTERVAL_MS,
1120
- buildLines,
1096
+ buildLines2 as buildLines,
1121
1097
  buildToolDefinitions,
1122
1098
  createDevSessionController,
1123
1099
  createShutdown,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku (beta) devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",
@@ -51,11 +51,18 @@ https://vibeview.io/docs/preparing-your-build.
51
51
  still missing (usually `--platform` or `--app`) — pass it and retry.
52
52
  4. Parse the `session_ready` line from stdout:
53
53
  ```json
54
- {"event":"session_ready","session_id":"...","url":"...","pid":1234}
54
+ {"event":"session_ready","session_id":"...","url":"...","pid":1234,"build":{...}}
55
55
  ```
56
56
  Use `session_id` as `--session <id>` on every verb below. The event
57
57
  fires once the session accepts commands (device up, app installed) —
58
- no need to poll `dev-status` first. If the app ships no native changes
58
+ no need to poll `dev-status` first. `build` says which build was
59
+ installed (`build.build`), why (`build.reason`: it prefers a debug build
60
+ of the project's own app version over a newer one of another version),
61
+ and the newest debug build of each other version (`build.other_debug_builds`).
62
+ If the app shows a red error screen right after start ("React Native
63
+ version mismatch", or "undefined is not a function" in a native module),
64
+ the build may be from other native code — rerun with `--build <id>` of
65
+ one of those. If the app ships no native changes
59
66
  since the last run, skip step 1 and just do steps 2–4.
60
67
  5. **Immediately tell the developer the session `url`** — "watch or take
61
68
  over at any time: `<url>`". The page is a live, fully interactive view
@@ -244,6 +251,9 @@ streaming minutes even when idle:
244
251
  ```bash
245
252
  vibeview dev-stop
246
253
  ```
254
+ If the `dev --detach` process is killed or the computer sleeps, its session
255
+ ends on its own within about 90 seconds (unless a browser tab is watching
256
+ it) — but don't rely on that; stop it.
247
257
  A session you did not start with `dev` (one created through the API, the
248
258
  sandbox, or a collaboration link) is stopped by id:
249
259
  ```bash
@@ -312,7 +322,7 @@ steps the CLI does with plain shell commands:
312
322
  |----------|------|---------|
313
323
  | `upload_app` | `file` (required path), `name` | Upload a build (zipped iOS/tvOS simulator `.app`, Android/Android TV `.apk`, Roku channel `.zip`). The MCP equivalent of `vibeview upload-app`, so step 1 of the loop needs no shell. Returns the `app_id` to pass to `dev_start`. |
314
324
  | `list_apps` | `platform` | List the org's apps (app id, name, platform, bundle id). The MCP equivalent of `vibeview list-apps` — use it to find an app someone else uploaded. |
315
- | `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`/`roku`), `app`, `metro_port`, `model`, `build_id`, `standalone` | Start a session held open by the MCP server. The MCP equivalent of `vibeview dev --detach`; `model` picks an exact device model (e.g. `"iPhone Duo"`); `build_id` runs that build instead of the newest debug one (see `list_builds`). `standalone: true` runs any build as it is with no Metro — a release, CI or cloud build (without `build_id`: the newest release build). An app for another platform is refused before a device is taken. Returns `page_url` — relay it to the developer immediately, same as step 5 of the loop — the device it got (model, OS, category; posture and lit screen on a foldable), and the build it installed (id, version, upload time) with the app's other debug builds. A red "React Native version mismatch" screen means that build was made with another React Native than the project: pick one made from this project with `build_id`. While every device is busy it waits in the queue, sending progress. |
325
+ | `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`/`roku`), `app`, `metro_port`, `model`, `build_id`, `standalone` | Start a session held open by the MCP server. The MCP equivalent of `vibeview dev --detach`; `model` picks an exact device model (e.g. `"iPhone Duo"`); `build_id` runs that build instead of the newest debug one (see `list_builds`). `standalone: true` runs any build as it is with no Metro — a release, CI or cloud build (without `build_id`: the newest release build). An app for another platform is refused before a device is taken. Returns `page_url` — relay it to the developer immediately, same as step 5 of the loop — the device it got (model, OS, category; posture and lit screen on a foldable), and the build it installed (id, version, upload time) with the app's other debug builds. It chooses the build the way `vibeview dev` does (a debug build of the project's own version over a newer one of another version) and says when it can't read the project's version. A red error screen right after start ("React Native version mismatch", or "undefined is not a function" in a native module) may mean the build was made from other native code: try one of the listed other builds with `build_id`. While every device is busy it waits in the queue, sending progress. |
316
326
  | `list_device_models` | `device_type` | List the device models you can start (model, OS, platform, category, `foldable` marker, and how many are free/busy). The MCP equivalent of `vibeview list-devices --models`. |
317
327
  | `list_builds` | `app` (required) | List an app's uploaded builds with their kind (`debug`/`release`/`unknown`). The MCP equivalent of `vibeview list-builds`. |
318
328
  | `dev_stop` | — | Stop the session `dev_start` started. The MCP equivalent of `vibeview dev-stop`. Says "no dev session was running" when there was none. |
@@ -329,5 +339,7 @@ targets that session instead of whatever `vibeview dev --detach` state exists
329
339
  in the project directory. Only one such session can be held at a time
330
340
  (`dev_start` fails if one is already running), and it stays live — billing
331
341
  streaming minutes — until `dev_stop` or the MCP connection ends (the server
332
- stops it when the client quits or closes the connection; a killed server
333
- can't). Always call `dev_stop` when you finish verifying.
342
+ stops it when the client quits or closes the connection; if the server is
343
+ killed or the computer sleeps, the session ends within about 90 seconds
344
+ unless a browser tab is watching it). Always call `dev_stop` when you
345
+ finish verifying.