vibeview 0.39.0 → 0.40.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.
@@ -274,7 +274,7 @@ var ApiClient = class {
274
274
  }
275
275
  async getRunStatus(runId) {
276
276
  const data = await this.request("GET", `/runs/${runId}`);
277
- return { ...data, url: data.run_url ?? `${this.baseUrl}/tests/runs/${runId}` };
277
+ return { ...data, url: runResultUrl(data, `${this.baseUrl}/tests/runs/${runId}`) };
278
278
  }
279
279
  /** Request without /ci prefix — for non-CI endpoints like /apps, /tests */
280
280
  async requestRaw(method, path, body) {
@@ -772,6 +772,13 @@ var ApiClient = class {
772
772
  return this.requestRaw("POST", `/webhooks/${id}/test`);
773
773
  }
774
774
  };
775
+ function runResultUrl(data, fallback) {
776
+ const url = data.run_url ?? fallback;
777
+ if (!data.retry_run_id) return url;
778
+ const firstId = url.slice(url.lastIndexOf("/") + 1);
779
+ if (!firstId) return url;
780
+ return `${url.slice(0, url.length - firstId.length)}${encodeURIComponent(data.retry_run_id)}`;
781
+ }
775
782
  function parseContentDispositionFilename(header) {
776
783
  if (!header) return void 0;
777
784
  const extMatch = header.match(/filename\*\s*=\s*[^']*''([^;]+)/i);
@@ -870,6 +877,10 @@ function createSpinner(text, opts = {}) {
870
877
  var EXIT_PASS = 0;
871
878
  var EXIT_TEST_FAIL = 1;
872
879
  var EXIT_INFRA_ERROR = 2;
880
+ var RUN_COMMANDS = ["run-test", "run-suite", "run-suites"];
881
+ function runCommandParseExitCode(err) {
882
+ return err.exitCode === 0 ? EXIT_PASS : EXIT_INFRA_ERROR;
883
+ }
873
884
  var UsageError = class extends Error {
874
885
  constructor(message) {
875
886
  super(message);
@@ -1015,6 +1026,8 @@ export {
1015
1026
  EXIT_PASS,
1016
1027
  EXIT_TEST_FAIL,
1017
1028
  EXIT_INFRA_ERROR,
1029
+ RUN_COMMANDS,
1030
+ runCommandParseExitCode,
1018
1031
  UsageError,
1019
1032
  DEVICE_STATUS_FILTERS,
1020
1033
  queueReasonCopy,
@@ -13,7 +13,7 @@ import {
13
13
  resolveArtifact,
14
14
  resolveAuth,
15
15
  uploadAppBuild
16
- } from "./chunk-MYXUGCR5.js";
16
+ } from "./chunk-GDTLLJFL.js";
17
17
 
18
18
  // src/lib/formatters/not-checked.ts
19
19
  function notCheckedSteps(steps) {
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  runAgentVerb,
31
31
  runStepsNotChecked,
32
32
  stepsNotCheckedLabel
33
- } from "./chunk-EQN3X62U.js";
33
+ } from "./chunk-MK3WDPVZ.js";
34
34
  import {
35
35
  ApiClient,
36
36
  ApiStatusError,
@@ -40,6 +40,7 @@ import {
40
40
  EXIT_TEST_FAIL,
41
41
  NetworkError,
42
42
  NotFoundError,
43
+ RUN_COMMANDS,
43
44
  UsageError,
44
45
  createSpinner,
45
46
  errorExitCode,
@@ -49,9 +50,10 @@ import {
49
50
  queueReasonCopy,
50
51
  reportCommandError,
51
52
  resolveAuth,
53
+ runCommandParseExitCode,
52
54
  saveConfig,
53
55
  uploadApp
54
- } from "./chunk-MYXUGCR5.js";
56
+ } from "./chunk-GDTLLJFL.js";
55
57
 
56
58
  // src/cli.ts
57
59
  import { createRequire } from "module";
@@ -125,9 +127,17 @@ function formatHuman(result, verbose) {
125
127
  const notCheckedCases = cases.filter((t) => outcomeOf(t) === "not_checked").length;
126
128
  const total = cases.length;
127
129
  const notCheckedNote = notCheckedCases > 0 ? `, ${notCheckedCases} not checked` : notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
128
- lines.push(` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}`)}`);
130
+ const quarantined = cases.filter((t) => t.status === "failed" && t.quarantined === true).length;
131
+ const quarantinedNote = quarantined > 0 && (result.test_runs ?? []).length > 0 ? `, ${quarantined} failed (quarantined)` : "";
132
+ lines.push(
133
+ ` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}${quarantinedNote}`)}`
134
+ );
129
135
  if (result.url) {
130
- lines.push(` ${chalk.dim(result.url)}`);
136
+ lines.push(` ${chalk.dim(result.first_attempt ? `Retry: ${result.url}` : result.url)}`);
137
+ }
138
+ if (result.first_attempt) {
139
+ const first = result.first_attempt;
140
+ lines.push(` ${chalk.dim(`First attempt (${first.status ?? "unknown"}): ${first.url}`)}`);
131
141
  }
132
142
  const suiteCases = result.test_runs ?? [];
133
143
  if (suiteCases.length > 0) {
@@ -1181,15 +1191,16 @@ Aggregate: ${aggregate}${aggregateNote}
1181
1191
  for (const cr of sr.case_runs ?? []) {
1182
1192
  const outcome = outcomeOf(cr);
1183
1193
  const neutral = outcome === "not_checked" || outcome === "not_run";
1184
- const caseMark = neutral ? chalk5.gray("\u25CB") : cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
1194
+ const caseMark = neutral ? chalk5.gray("\u25CB") : cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : cr.status === "failed" && cr.quarantined === true ? chalk5.yellow("\u2717") : chalk5.red("\u2717");
1185
1195
  const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
1186
1196
  const unchecked = notCheckedSuffix(outcome, cr.steps_not_checked);
1187
- const label = neutral ? outcome.replace("_", " ") : cr.status ?? "?";
1197
+ const quarantined = cr.status === "failed" && cr.quarantined === true;
1198
+ const label = neutral ? outcome.replace("_", " ") : quarantined ? "failed (quarantined)" : cr.status ?? "?";
1188
1199
  process.stdout.write(
1189
1200
  ` ${caseMark} ${cr.case_name ?? "Case"} ${label}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
1190
1201
  `
1191
1202
  );
1192
- if (cr.error_message && neutral) {
1203
+ if (cr.error_message && (neutral || cr.status === "failed" && cr.quarantined === true)) {
1193
1204
  process.stdout.write(` ${chalk5.dim(cr.error_message)}
1194
1205
  `);
1195
1206
  } else if (cr.error_message && cr.status !== "passed") {
@@ -1205,7 +1216,15 @@ Aggregate: ${aggregate}${aggregateNote}
1205
1216
  }
1206
1217
  function suiteCounts(sr) {
1207
1218
  const nc = sr.not_checked_tests ?? 0;
1208
- return `${sr.passed} passed, ${sr.failed} failed${nc > 0 ? `, ${nc} not checked` : ""}`;
1219
+ const q = Math.min(
1220
+ sr.failed,
1221
+ sr.quarantined_failures ?? (sr.case_runs ?? []).filter((c) => c.status === "failed" && c.quarantined === true).length
1222
+ );
1223
+ const parts = [`${sr.passed} passed`];
1224
+ if (q === 0 || sr.failed > q) parts.push(`${sr.failed - q} failed`);
1225
+ if (q > 0) parts.push(`${q} failed (quarantined)`);
1226
+ if (nc > 0) parts.push(`${nc} not checked`);
1227
+ return parts.join(", ");
1209
1228
  }
1210
1229
 
1211
1230
  // src/commands/run-test.ts
@@ -4387,7 +4406,7 @@ program.command("dev-reload").description(
4387
4406
  program.command("mcp").description(
4388
4407
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4389
4408
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4390
- const { runMcpServer } = await import("./server-GEY4WZUH.js");
4409
+ const { runMcpServer } = await import("./server-56MR4KJW.js");
4391
4410
  await runMcpServer({ ...opts, version: pkg.version });
4392
4411
  });
4393
4412
  program.command("agent-setup").description(
@@ -4411,6 +4430,9 @@ ${pressButtonsHelp()}`);
4411
4430
  return runAgentVerb(cmd, positionals, opts);
4412
4431
  });
4413
4432
  }
4433
+ for (const name of RUN_COMMANDS) {
4434
+ program.commands.find((c) => c.name() === name)?.exitOverride((err) => process.exit(runCommandParseExitCode(err)));
4435
+ }
4414
4436
  if (updateCheckAllowed(process.argv, process.env, process.stderr.isTTY)) {
4415
4437
  void checkForUpdate(pkg.version, { print: (line) => process.stderr.write(`${line}
4416
4438
  `) });
@@ -23,18 +23,19 @@ import {
23
23
  outcomeOf,
24
24
  parseBuildId,
25
25
  parseIdleWarningSeconds,
26
+ platformLabel,
26
27
  platformWithArticle,
27
28
  readDevState,
28
29
  recordDevUse,
29
30
  renderEnvelope,
30
31
  resolveMetro,
31
32
  startDevSession
32
- } from "./chunk-EQN3X62U.js";
33
+ } from "./chunk-MK3WDPVZ.js";
33
34
  import {
34
35
  ApiClient,
35
36
  NotFoundError,
36
37
  resolveAuth
37
- } from "./chunk-MYXUGCR5.js";
38
+ } from "./chunk-GDTLLJFL.js";
38
39
 
39
40
  // src/mcp/server.ts
40
41
  import { writeFileSync } from "fs";
@@ -59,7 +60,12 @@ var CREATE_TEST_DEF = {
59
60
  app: { type: "string", description: "App id (from list_apps)." },
60
61
  name: { type: "string", description: "Test name." },
61
62
  steps: { type: "string", description: "The steps, one per line." },
62
- group: { type: "string", description: "Group id to add the test to (optional)." }
63
+ group: { type: "string", description: "Group id to add the test to (optional)." },
64
+ if_exists: {
65
+ type: "string",
66
+ description: "When a test with this name already exists for the app: update replaces it, new creates another one beside it, fail refuses. Omitted: an identical test is reused, a different one is refused.",
67
+ enum: ["fail", "update", "new"]
68
+ }
63
69
  },
64
70
  required: ["app", "name", "steps"]
65
71
  };
@@ -72,7 +78,7 @@ var RUN_TEST_DEF = {
72
78
  session_id: { type: "string", description: "Run on this session instead of a new device." },
73
79
  device_model: {
74
80
  type: "string",
75
- description: "Device model for a new device (from list_device_models); omitted: any free one."
81
+ description: "Device model for a new device (from list_device_models); omitted: any free one. A model of another platform (same form factor) runs the test's app for that platform: the app linked in its app family, else the app with the same bundle id / package. The reply names the platform and app the run went to."
76
82
  },
77
83
  ai_use: AI_USE_PROPERTY,
78
84
  start_from_current_screen: {
@@ -94,7 +100,7 @@ var RUN_GROUP_DEF = {
94
100
  session_id: { type: "string", description: "Run on this session instead of a new device." },
95
101
  device_model: {
96
102
  type: "string",
97
- description: "Device model for a new device; omitted: any free one."
103
+ description: "Device model for a new device; omitted: any free one. A model of another platform (same form factor) runs the group's app for that platform: the app linked in its app family, else the app with the same bundle id / package. The reply names the platform and app the run went to."
98
104
  },
99
105
  ai_use: AI_USE_PROPERTY,
100
106
  start_from_current_screen: {
@@ -347,6 +353,14 @@ async function explainInactive(sessionId, deps) {
347
353
  function findCommand(toolName) {
348
354
  return AGENT_COMMANDS.find((c) => toolNameFor(c.verb) === toolName);
349
355
  }
356
+ function testToolErrorMessage(err) {
357
+ const message = err instanceof Error ? err.message : String(err);
358
+ if (err instanceof NotFoundError && message.startsWith("Not found: ")) {
359
+ const detail = message.slice("Not found: ".length);
360
+ if (/not found/i.test(detail)) return detail;
361
+ }
362
+ return message;
363
+ }
350
364
  async function handleCreateTest(args, deps) {
351
365
  const steps = String(args.steps ?? "").split("\n").map((s) => s.trim()).filter(Boolean).map((text) => ({ text }));
352
366
  if (!steps.length) return errorResult("create_test: `steps` needs at least one line");
@@ -355,7 +369,8 @@ async function handleCreateTest(args, deps) {
355
369
  name: String(args.name),
356
370
  app_id: String(args.app),
357
371
  steps,
358
- group_ids: typeof args.group === "string" && args.group ? [args.group] : void 0
372
+ group_ids: typeof args.group === "string" && args.group ? [args.group] : void 0,
373
+ if_exists: args.if_exists === "fail" || args.if_exists === "update" || args.if_exists === "new" ? args.if_exists : void 0
359
374
  });
360
375
  const verb = created.outcome === "unchanged" ? "Test already exists, unchanged" : created.outcome === "updated" ? "Updated existing test" : "Created test";
361
376
  return textResult(
@@ -363,9 +378,17 @@ async function handleCreateTest(args, deps) {
363
378
  created
364
379
  );
365
380
  } catch (err) {
366
- return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
381
+ return errorResult(`error: ${testToolErrorMessage(err)}`);
367
382
  }
368
383
  }
384
+ function runDestination(lanes) {
385
+ return lanes.map((l) => {
386
+ const label = platformLabel(l.platform);
387
+ const device = l.model ? ` (${l.model})` : "";
388
+ const app = l.app_name ? `: ${l.app_name}, the ${label} app` : "";
389
+ return ` Running on ${label}${device}${app}.`;
390
+ }).join("");
391
+ }
369
392
  async function handleRun(kind, args, deps) {
370
393
  const id = String(kind === "test" ? args.test : args.group);
371
394
  const mode = typeof args.ai_use === "string" ? AI_USE_MODE[args.ai_use] : void 0;
@@ -387,12 +410,13 @@ async function handleRun(kind, args, deps) {
387
410
  kind === "test" ? { target: "test", test_ids: [id], devices, mode } : { target: "group", group_id: id, devices, mode }
388
411
  );
389
412
  const queued = r.lanes.some((l) => l.status === "queued") ? " It is waiting for a free device." : "";
413
+ const where = runDestination(r.lanes);
390
414
  return textResult(
391
- `Started run ${r.batch_id}.${queued} Check it with get_run run_id=${r.batch_id}.`,
415
+ `Started run ${r.batch_id}.${where}${queued} Check it with get_run run_id=${r.batch_id}.`,
392
416
  r
393
417
  );
394
418
  } catch (err) {
395
- return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
419
+ return errorResult(`error: ${testToolErrorMessage(err)}`);
396
420
  }
397
421
  }
398
422
  function notCheckedNote(status, n) {
@@ -407,11 +431,25 @@ function agentStatus(r) {
407
431
  function testsNotChecked(n) {
408
432
  return typeof n === "number" && n > 0 ? `, ${n} not checked` : "";
409
433
  }
434
+ function quarantinedFailure(t) {
435
+ return t?.status === "failed" && t?.quarantined === true;
436
+ }
437
+ function quarantinedCount(n) {
438
+ return typeof n === "number" && n > 0 ? `, ${n} failed (quarantined)` : "";
439
+ }
410
440
  function formatRunForAgent(run) {
411
441
  const lines = [];
412
442
  if (run.lanes) {
443
+ const rowQuarantined = (r) => typeof r.tests_quarantined === "number" ? r.tests_quarantined : (r.tests ?? []).filter(quarantinedFailure).length;
444
+ let totalQuarantined = run.summary.tests_quarantined;
445
+ if (typeof totalQuarantined !== "number") {
446
+ totalQuarantined = 0;
447
+ for (const lane of run.lanes) {
448
+ for (const r of lane.runs ?? []) totalQuarantined += rowQuarantined(r);
449
+ }
450
+ }
413
451
  lines.push(
414
- `${run.name}: ${agentStatus(run)} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed${testsNotChecked(run.summary.tests_not_checked)}`
452
+ `${run.name}: ${agentStatus(run)} \u2014 ${run.summary.tests_passed} of ${run.summary.tests_total} tests passed${testsNotChecked(run.summary.tests_not_checked)}${quarantinedCount(totalQuarantined)}`
415
453
  );
416
454
  for (const lane of run.lanes) {
417
455
  const detail = [lane.platform, lane.os_version].filter(Boolean).join(" ");
@@ -422,10 +460,19 @@ function formatRunForAgent(run) {
422
460
  if (lane.error_message) head += ` \u2014 ${lane.error_message}`;
423
461
  lines.push(head);
424
462
  for (const r of lane.runs ?? []) {
425
- const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed${testsNotChecked(r.tests_not_checked)})` : "";
463
+ const count = r.kind === "group" && typeof r.tests_total === "number" ? ` (${r.tests_passed ?? 0}/${r.tests_total} tests passed${testsNotChecked(r.tests_not_checked)}${quarantinedCount(rowQuarantined(r))})` : "";
464
+ const q = r.kind === "test" && quarantinedFailure(r.tests?.[0] ?? {}) ? " (quarantined)" : "";
426
465
  lines.push(
427
- ` ${r.name}: ${agentStatus(r)}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
466
+ ` ${r.name}: ${agentStatus(r)}${q}${count}${r.error_message ? ` \u2014 ${r.error_message}` : ""}`
428
467
  );
468
+ if (r.kind !== "group") continue;
469
+ for (const t of r.tests ?? []) {
470
+ if (t.status !== "failed" && t.status !== "error") continue;
471
+ const tq = quarantinedFailure(t) ? " (quarantined)" : "";
472
+ lines.push(
473
+ ` ${t.name}: ${t.status}${tq}${t.error_message ? ` \u2014 ${t.error_message}` : ""}`
474
+ );
475
+ }
429
476
  }
430
477
  }
431
478
  } else if (Array.isArray(run.test_runs)) {
@@ -463,7 +510,7 @@ async function handleGetRun(args, deps) {
463
510
  const run = kind.kind === "batch" ? await client.getRunBatch(id) : await client.getRunStatus(id);
464
511
  return textResult(formatRunForAgent(run), run);
465
512
  } catch (err) {
466
- return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
513
+ return errorResult(`error: ${testToolErrorMessage(err)}`);
467
514
  }
468
515
  }
469
516
  async function dispatchToolCall(toolName, args, deps, extra = {}) {
@@ -882,7 +929,7 @@ async function handleUploadApp(args, deps) {
882
929
  }
883
930
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
884
931
  const upload = deps.uploadApp ?? (async (f, n) => {
885
- const { uploadAppBuild } = await import("./upload-app-UHJLFQKZ.js");
932
+ const { uploadAppBuild } = await import("./upload-app-ZEQMGZIT.js");
886
933
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
887
934
  return result2;
888
935
  });
@@ -1278,6 +1325,23 @@ var MCP_INSTRUCTIONS = [
1278
1325
  "A session stopped elsewhere is reported as ended; call dev_start again for a new one.",
1279
1326
  'A session nobody uses ends after the idle timeout. A result that ends with "Session \u2026 ends in N s for inactivity" means act (tap, type, swipe, press) to keep it, or dev_stop if you are done; ui_tree, screenshot, logs, find and wait do not count as use.'
1280
1327
  ].join("\n");
1328
+ function toolInputSchema(def, z) {
1329
+ const shape = {};
1330
+ for (const [key, prop] of Object.entries(def.properties)) {
1331
+ const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
1332
+ const described = base.describe(prop.description);
1333
+ shape[key] = def.required.includes(key) ? described : described.optional();
1334
+ }
1335
+ const valid = Object.keys(def.properties);
1336
+ return z.strictObject(shape, {
1337
+ error: (issue) => {
1338
+ if (issue.code !== "unrecognized_keys") return void 0;
1339
+ const unknown = issue.keys.map((k) => `"${k}"`).join(", ");
1340
+ const takes = valid.length ? `${def.name} takes: ${valid.join(", ")}.` : `${def.name} takes no arguments.`;
1341
+ return `Unknown argument${issue.keys.length === 1 ? "" : "s"} ${unknown}. ${takes}`;
1342
+ }
1343
+ });
1344
+ }
1281
1345
  async function createMcpServer(opts) {
1282
1346
  const [{ McpServer }, { z }] = await Promise.all([
1283
1347
  import("@modelcontextprotocol/sdk/server/mcp.js"),
@@ -1296,21 +1360,12 @@ async function createMcpServer(opts) {
1296
1360
  { name: "vibeview", version: opts.version ?? "0.0.0" },
1297
1361
  { instructions: MCP_INSTRUCTIONS }
1298
1362
  );
1299
- function zodShape(def) {
1300
- const shape = {};
1301
- for (const [key, prop] of Object.entries(def.properties)) {
1302
- const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
1303
- const described = base.describe(prop.description);
1304
- shape[key] = def.required.includes(key) ? described : described.optional();
1305
- }
1306
- return shape;
1307
- }
1308
1363
  for (const def of buildToolDefinitions()) {
1309
1364
  server.registerTool(
1310
1365
  def.name,
1311
1366
  {
1312
1367
  description: def.description,
1313
- inputSchema: zodShape(def),
1368
+ inputSchema: toolInputSchema(def, z),
1314
1369
  annotations: { readOnlyHint: !def.mutating }
1315
1370
  },
1316
1371
  async (args, extra) => {
@@ -1397,7 +1452,10 @@ export {
1397
1452
  idleNoticeText,
1398
1453
  mcpText,
1399
1454
  progressReporter,
1455
+ runDestination,
1400
1456
  runMcpServer,
1401
1457
  startProgress,
1402
- startedDevice
1458
+ startedDevice,
1459
+ testToolErrorMessage,
1460
+ toolInputSchema
1403
1461
  };
@@ -3,7 +3,7 @@ import {
3
3
  describeBuild,
4
4
  uploadApp,
5
5
  uploadAppBuild
6
- } from "./chunk-MYXUGCR5.js";
6
+ } from "./chunk-GDTLLJFL.js";
7
7
  export {
8
8
  describeBuild,
9
9
  uploadApp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku 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",