vibeview 0.27.1 → 0.28.1
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/{chunk-7IOGMM5W.js → chunk-3DKY443B.js} +154 -123
- package/dist/{chunk-FTTWLCTE.js → chunk-PBMB6TG4.js} +120 -15
- package/dist/cli.js +386 -90
- package/dist/{server-ZIUZ7U5T.js → server-BGQJOVSN.js} +298 -53
- package/dist/{upload-app-MDKTSG4O.js → upload-app-FEZG23NU.js} +1 -1
- package/package.json +2 -2
- package/skills/vibeview-agent/SKILL.md +10 -5
package/dist/cli.js
CHANGED
|
@@ -22,13 +22,15 @@ import {
|
|
|
22
22
|
readDevState,
|
|
23
23
|
readMetroState,
|
|
24
24
|
runAgentVerb
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-3DKY443B.js";
|
|
26
26
|
import {
|
|
27
27
|
ApiClient,
|
|
28
|
+
ApiStatusError,
|
|
28
29
|
DEVICE_STATUS_FILTERS,
|
|
29
30
|
EXIT_INFRA_ERROR,
|
|
30
31
|
EXIT_PASS,
|
|
31
32
|
EXIT_TEST_FAIL,
|
|
33
|
+
NetworkError,
|
|
32
34
|
NotFoundError,
|
|
33
35
|
UsageError,
|
|
34
36
|
createSpinner,
|
|
@@ -36,11 +38,12 @@ import {
|
|
|
36
38
|
formatUtcTimestamp,
|
|
37
39
|
loadConfig,
|
|
38
40
|
printJsonError,
|
|
41
|
+
queueReasonCopy,
|
|
39
42
|
reportCommandError,
|
|
40
43
|
resolveAuth,
|
|
41
44
|
saveConfig,
|
|
42
45
|
uploadApp
|
|
43
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-PBMB6TG4.js";
|
|
44
47
|
|
|
45
48
|
// src/cli.ts
|
|
46
49
|
import { createRequire } from "module";
|
|
@@ -94,11 +97,8 @@ function formatHuman(result, verbose) {
|
|
|
94
97
|
const icon = result.status === "passed" ? chalk.green("PASS") : chalk.red("FAIL");
|
|
95
98
|
const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
96
99
|
lines.push("");
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} else {
|
|
100
|
-
lines.push(` ${icon} ${chalk.bold(result.run_id)} ${chalk.dim(duration)}`);
|
|
101
|
-
}
|
|
100
|
+
const title = result.suite_name || result.case_name || result.run_id;
|
|
101
|
+
lines.push(` ${icon} ${chalk.bold(title)} ${chalk.dim(duration)}`);
|
|
102
102
|
const cases = resultCases(result);
|
|
103
103
|
const passed = cases.filter((t) => t.status === "passed").length;
|
|
104
104
|
const total = cases.length;
|
|
@@ -123,7 +123,7 @@ function formatHuman(result, verbose) {
|
|
|
123
123
|
}
|
|
124
124
|
if (result.steps && result.steps.length > 0) {
|
|
125
125
|
lines.push("");
|
|
126
|
-
if (result.case_name) {
|
|
126
|
+
if (result.case_name && result.case_name !== title) {
|
|
127
127
|
lines.push(` ${chalk.bold(result.case_name)}`);
|
|
128
128
|
}
|
|
129
129
|
for (const step of result.steps) {
|
|
@@ -173,31 +173,64 @@ function formatJunit(result) {
|
|
|
173
173
|
lines.push(
|
|
174
174
|
` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}" timestamp="${timestamp}">`
|
|
175
175
|
);
|
|
176
|
+
if (result.url) {
|
|
177
|
+
lines.push(" <properties>");
|
|
178
|
+
lines.push(` <property name="vibeview.run_url" value="${escapeXml(result.url)}" />`);
|
|
179
|
+
lines.push(" </properties>");
|
|
180
|
+
}
|
|
176
181
|
for (const tr of cases) {
|
|
177
182
|
const caseName = escapeXml(tr.case_name);
|
|
178
183
|
const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
|
|
179
|
-
|
|
184
|
+
const out = tr.url ? ` <system-out>${escapeXml(tr.url)}</system-out>` : null;
|
|
185
|
+
if (tr.status === "passed" && !out) {
|
|
180
186
|
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
|
|
181
|
-
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
190
|
+
if (tr.status === "failed") {
|
|
182
191
|
const msg = escapeXml(tr.error_message || "Test failed");
|
|
183
|
-
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
184
192
|
lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
|
|
185
|
-
|
|
186
|
-
} else {
|
|
193
|
+
} else if (tr.status !== "passed") {
|
|
187
194
|
const msg = escapeXml(tr.error_message || "Infrastructure error");
|
|
188
|
-
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
189
195
|
lines.push(` <error message="${msg}" type="InfraError">${msg}</error>`);
|
|
190
|
-
lines.push(" </testcase>");
|
|
191
196
|
}
|
|
197
|
+
if (out) lines.push(out);
|
|
198
|
+
lines.push(" </testcase>");
|
|
192
199
|
}
|
|
193
200
|
lines.push(" </testsuite>");
|
|
194
201
|
lines.push("</testsuites>");
|
|
195
202
|
return lines.join("\n");
|
|
196
203
|
}
|
|
197
|
-
function
|
|
204
|
+
function formatJunitError(name, message, url) {
|
|
205
|
+
const n = escapeXml(name);
|
|
206
|
+
const msg = escapeXml(message);
|
|
207
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
208
|
+
const lines = [];
|
|
209
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
210
|
+
lines.push(`<testsuites name="vibeview" tests="1" failures="0" errors="1" time="0">`);
|
|
211
|
+
lines.push(
|
|
212
|
+
` <testsuite name="${n}" tests="1" failures="0" errors="1" time="0" timestamp="${timestamp}">`
|
|
213
|
+
);
|
|
214
|
+
if (url) {
|
|
215
|
+
lines.push(" <properties>");
|
|
216
|
+
lines.push(` <property name="vibeview.run_url" value="${escapeXml(url)}" />`);
|
|
217
|
+
lines.push(" </properties>");
|
|
218
|
+
}
|
|
219
|
+
lines.push(` <testcase name="${n}" classname="${n}" time="0">`);
|
|
220
|
+
lines.push(` <error message="${msg}" type="InfraError">${msg}</error>`);
|
|
221
|
+
lines.push(" </testcase>");
|
|
222
|
+
lines.push(" </testsuite>");
|
|
223
|
+
lines.push("</testsuites>");
|
|
224
|
+
return lines.join("\n");
|
|
225
|
+
}
|
|
226
|
+
var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
227
|
+
function formatMultiSuiteJunit(result, opts = {}) {
|
|
228
|
+
const unfinished = (status) => opts.timedOut !== void 0 && !MULTI_TERMINAL.has(status);
|
|
229
|
+
const errored = (status) => status === "error" || unfinished(status);
|
|
198
230
|
const tests = result.suite_runs.reduce((sum, sr) => sum + sr.passed + sr.failed, 0);
|
|
199
231
|
const failures = result.suite_runs.reduce((sum, sr) => sum + sr.failed, 0);
|
|
200
|
-
const
|
|
232
|
+
const noSuites = opts.timedOut !== void 0 && result.suite_runs.length === 0;
|
|
233
|
+
const totalErrors = result.suite_runs.filter((sr) => errored(sr.status)).length + (noSuites ? 1 : 0);
|
|
201
234
|
const timestamp = result.started_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
202
235
|
const lines = [];
|
|
203
236
|
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
@@ -208,43 +241,114 @@ function formatMultiSuiteJunit(result) {
|
|
|
208
241
|
const suiteName = escapeXml(sr.suite_name || sr.suite_id);
|
|
209
242
|
const suiteTests = sr.passed + sr.failed;
|
|
210
243
|
const suiteFails = sr.failed;
|
|
211
|
-
const suiteErrors = sr.status
|
|
244
|
+
const suiteErrors = errored(sr.status) ? 1 : 0;
|
|
212
245
|
lines.push(
|
|
213
246
|
` <testsuite name="${suiteName}" tests="${suiteTests}" failures="${suiteFails}" errors="${suiteErrors}" timestamp="${timestamp}">`
|
|
214
247
|
);
|
|
215
|
-
if (
|
|
248
|
+
if (sr.url) {
|
|
249
|
+
lines.push(" <properties>");
|
|
250
|
+
lines.push(` <property name="vibeview.run_url" value="${escapeXml(sr.url)}" />`);
|
|
251
|
+
lines.push(" </properties>");
|
|
252
|
+
}
|
|
253
|
+
if (unfinished(sr.status)) {
|
|
254
|
+
const msg = escapeXml(`${opts.timedOut} (suite was ${sr.status})`);
|
|
255
|
+
lines.push(` <error message="${msg}" type="InfraError" />`);
|
|
256
|
+
} else if (suiteErrors > 0) {
|
|
216
257
|
lines.push(` <error message="Suite errored" type="InfraError" />`);
|
|
217
258
|
}
|
|
218
259
|
lines.push(" </testsuite>");
|
|
219
260
|
}
|
|
261
|
+
if (noSuites) {
|
|
262
|
+
const msg = escapeXml(opts.timedOut);
|
|
263
|
+
lines.push(
|
|
264
|
+
` <testsuite name="vibeview run-suites" tests="0" failures="0" errors="1" timestamp="${timestamp}">`
|
|
265
|
+
);
|
|
266
|
+
lines.push(` <error message="${msg}" type="InfraError" />`);
|
|
267
|
+
lines.push(" </testsuite>");
|
|
268
|
+
}
|
|
220
269
|
lines.push("</testsuites>");
|
|
221
270
|
return lines.join("\n");
|
|
222
271
|
}
|
|
223
272
|
|
|
224
273
|
// src/lib/poll.ts
|
|
225
|
-
function
|
|
226
|
-
|
|
274
|
+
function resumeSpinner(spinner, text) {
|
|
275
|
+
if (spinner.isEnabled === false) {
|
|
276
|
+
if (text !== void 0) spinner.text = text;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
spinner.start(text);
|
|
280
|
+
}
|
|
281
|
+
function printBelowSpinner(spinner, line, nextText) {
|
|
282
|
+
const text = nextText ?? spinner.text;
|
|
227
283
|
spinner.stop();
|
|
228
284
|
process.stderr.write(line + "\n");
|
|
229
|
-
spinner
|
|
285
|
+
resumeSpinner(spinner, text);
|
|
286
|
+
}
|
|
287
|
+
function printStepLines(stepNum, total, status, text, actions, error) {
|
|
288
|
+
const icon = status === "passed" ? chalk2.green("\u2713") : status === "failed" ? chalk2.red("\u2717") : chalk2.yellow("!");
|
|
289
|
+
process.stderr.write(` ${icon} Step ${stepNum}/${total}: ${text}
|
|
290
|
+
`);
|
|
291
|
+
for (const action of actions ?? []) {
|
|
292
|
+
process.stderr.write(` ${chalk2.dim(formatAction(action))}
|
|
293
|
+
`);
|
|
294
|
+
}
|
|
295
|
+
if (error) process.stderr.write(` ${chalk2.red(error)}
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
var PollTimeoutError = class extends Error {
|
|
299
|
+
constructor(message) {
|
|
300
|
+
super(message);
|
|
301
|
+
this.name = "PollTimeoutError";
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var POLL_RETRY_DELAYS_MS = [2e3, 4e3, 8e3, 16e3, 3e4];
|
|
305
|
+
function isTransientPollError(err) {
|
|
306
|
+
if (err instanceof NetworkError) return true;
|
|
307
|
+
if (err instanceof ApiStatusError) {
|
|
308
|
+
return err.status === 408 || err.status === 429 || err.status >= 500;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
async function retryTransient(fn, opts = {}) {
|
|
313
|
+
for (let attempt = 0; ; attempt++) {
|
|
314
|
+
try {
|
|
315
|
+
return await fn();
|
|
316
|
+
} catch (err) {
|
|
317
|
+
const delay = POLL_RETRY_DELAYS_MS[attempt];
|
|
318
|
+
if (!isTransientPollError(err) || delay === void 0) throw err;
|
|
319
|
+
if (opts.deadline !== void 0 && Date.now() + delay >= opts.deadline) throw err;
|
|
320
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
321
|
+
process.stderr.write(
|
|
322
|
+
`${opts.label ?? "Status poll"} failed (${message}); retrying in ${delay / 1e3}s (${attempt + 1}/${POLL_RETRY_DELAYS_MS.length})
|
|
323
|
+
`
|
|
324
|
+
);
|
|
325
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function keepAliveEnabled(output) {
|
|
330
|
+
return output !== "json" && output !== "junit";
|
|
230
331
|
}
|
|
231
|
-
async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObserved) {
|
|
332
|
+
async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObserved, opts = {}) {
|
|
232
333
|
const deadline = Date.now() + timeoutMs;
|
|
233
334
|
const isCI = process.env.CI === "true" || process.env.CI === "1" || !process.stdout.isTTY;
|
|
335
|
+
const keepAlive = isCI && opts.keepAlive !== false;
|
|
234
336
|
let lastLogTime = Date.now();
|
|
235
337
|
let lastSeenEventCount = 0;
|
|
236
338
|
let totalSteps;
|
|
237
339
|
let currentStepLabel = "";
|
|
340
|
+
const printedSteps = /* @__PURE__ */ new Set();
|
|
238
341
|
let cachedDeviceLabel = null;
|
|
342
|
+
let lastQueueReason = null;
|
|
239
343
|
const caseStateBefore = /* @__PURE__ */ new Map();
|
|
240
344
|
const caseStartedPrinted = /* @__PURE__ */ new Set();
|
|
241
345
|
while (Date.now() < deadline) {
|
|
242
|
-
const result = await client.getRunStatus(runId);
|
|
346
|
+
const result = await retryTransient(() => client.getRunStatus(runId), { deadline });
|
|
243
347
|
const progress = result.progress || [];
|
|
244
348
|
if (progress.length > lastSeenEventCount) {
|
|
245
349
|
const newEvents = progress.slice(lastSeenEventCount);
|
|
246
350
|
for (const evt of newEvents) {
|
|
247
|
-
const ctx = { spinner, totalSteps, currentStepLabel };
|
|
351
|
+
const ctx = { spinner, totalSteps, currentStepLabel, printedSteps };
|
|
248
352
|
displayProgressEvent(evt, ctx);
|
|
249
353
|
totalSteps = ctx.totalSteps;
|
|
250
354
|
currentStepLabel = ctx.currentStepLabel;
|
|
@@ -277,6 +381,20 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
277
381
|
}
|
|
278
382
|
}
|
|
279
383
|
if (["passed", "failed", "error", "cancelled"].includes(result.status)) {
|
|
384
|
+
if (printedSteps.size > 0 && Array.isArray(result.steps)) {
|
|
385
|
+
spinner.stop();
|
|
386
|
+
result.steps.forEach((st, i) => {
|
|
387
|
+
if (printedSteps.has(i)) return;
|
|
388
|
+
printStepLines(
|
|
389
|
+
i + 1,
|
|
390
|
+
totalSteps ?? result.steps.length,
|
|
391
|
+
st.status,
|
|
392
|
+
st.step,
|
|
393
|
+
st.actions,
|
|
394
|
+
st.error
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
280
398
|
return result;
|
|
281
399
|
}
|
|
282
400
|
if (result.status === "queued" && result.queue) {
|
|
@@ -299,6 +417,11 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
299
417
|
label = "the device pool";
|
|
300
418
|
}
|
|
301
419
|
spinner.text = `Queued \u2014 position ${q.position} in line for ${label}`;
|
|
420
|
+
const why = queueReasonCopy(q.reason);
|
|
421
|
+
if (why && why !== lastQueueReason) {
|
|
422
|
+
printBelowSpinner(spinner, `${chalk2.yellow("\u23F3")} Waiting for a device: ${why}`);
|
|
423
|
+
}
|
|
424
|
+
lastQueueReason = why;
|
|
302
425
|
}
|
|
303
426
|
if (progress.length === 0) {
|
|
304
427
|
if (result.status === "pending") {
|
|
@@ -317,20 +440,25 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
317
440
|
spinner.text = `Status: ${result.status}...`;
|
|
318
441
|
}
|
|
319
442
|
}
|
|
320
|
-
if (
|
|
321
|
-
|
|
443
|
+
if (keepAlive && Date.now() - lastLogTime >= 3e4) {
|
|
444
|
+
const why = result.status === "queued" ? queueReasonCopy(result.queue?.reason) : null;
|
|
445
|
+
const where = result.status === "queued" && result.queue ? ` (position ${result.queue.position} in line for a device${why ? `: ${why}` : ""})` : "";
|
|
446
|
+
process.stderr.write(`Status: ${result.status}${where}...
|
|
447
|
+
`);
|
|
322
448
|
lastLogTime = Date.now();
|
|
323
449
|
}
|
|
324
450
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
325
451
|
}
|
|
326
|
-
throw new
|
|
452
|
+
throw new PollTimeoutError(
|
|
453
|
+
`Timeout: run did not complete within ${Math.round(timeoutMs / 1e3)}s`
|
|
454
|
+
);
|
|
327
455
|
}
|
|
328
456
|
function formatAction(action) {
|
|
329
457
|
const tool = action.tool;
|
|
330
458
|
const args = action.args || {};
|
|
331
459
|
switch (tool) {
|
|
332
460
|
case "tap":
|
|
333
|
-
return `tap ${args.element_ref
|
|
461
|
+
return `tap ${args.ref ?? args.element_ref ?? ""}`.trimEnd();
|
|
334
462
|
case "tap_coordinates":
|
|
335
463
|
return `tap (${args.x}, ${args.y})`;
|
|
336
464
|
case "type_text":
|
|
@@ -358,7 +486,8 @@ function displayProgressEvent(evt, ctx) {
|
|
|
358
486
|
case "run_started":
|
|
359
487
|
printBelowSpinner(
|
|
360
488
|
spinner,
|
|
361
|
-
`${chalk2.cyan("\u25B6")} AI test started \u2014 ${evt.total_steps} step(s), model: ${chalk2.bold(evt.model || "default")}
|
|
489
|
+
`${chalk2.cyan("\u25B6")} AI test started \u2014 ${evt.total_steps} step(s), model: ${chalk2.bold(evt.model || "default")}`,
|
|
490
|
+
" Starting test..."
|
|
362
491
|
);
|
|
363
492
|
ctx.totalSteps = evt.total_steps ?? ctx.totalSteps;
|
|
364
493
|
break;
|
|
@@ -370,23 +499,17 @@ function displayProgressEvent(evt, ctx) {
|
|
|
370
499
|
break;
|
|
371
500
|
}
|
|
372
501
|
case "step_completed": {
|
|
373
|
-
const
|
|
502
|
+
const stepIndex = evt.step_index ?? 0;
|
|
503
|
+
const stepNum = stepIndex + 1;
|
|
374
504
|
const total = ctx.totalSteps ?? evt.total_steps ?? "?";
|
|
375
|
-
|
|
505
|
+
if (ctx.printedSteps.has(stepIndex)) break;
|
|
506
|
+
ctx.printedSteps.add(stepIndex);
|
|
376
507
|
spinner.stop();
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if (evt.error) {
|
|
386
|
-
process.stderr.write(` ${chalk2.red(evt.error)}
|
|
387
|
-
`);
|
|
388
|
-
}
|
|
389
|
-
spinner.start();
|
|
508
|
+
printStepLines(stepNum, total, evt.status, evt.step_text || "", evt.actions, evt.error);
|
|
509
|
+
resumeSpinner(
|
|
510
|
+
spinner,
|
|
511
|
+
typeof total === "number" && stepNum >= total ? " Finishing..." : " Next step..."
|
|
512
|
+
);
|
|
390
513
|
break;
|
|
391
514
|
}
|
|
392
515
|
case "conversation_turn": {
|
|
@@ -405,7 +528,7 @@ function displayProgressEvent(evt, ctx) {
|
|
|
405
528
|
`${icon} Run ${evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
|
|
406
529
|
`
|
|
407
530
|
);
|
|
408
|
-
spinner
|
|
531
|
+
resumeSpinner(spinner);
|
|
409
532
|
break;
|
|
410
533
|
}
|
|
411
534
|
case "app_installing":
|
|
@@ -418,6 +541,17 @@ function displayProgressEvent(evt, ctx) {
|
|
|
418
541
|
break;
|
|
419
542
|
}
|
|
420
543
|
}
|
|
544
|
+
function outputRunError(err, options, ctx) {
|
|
545
|
+
if (options.output === "json") {
|
|
546
|
+
const extra = {};
|
|
547
|
+
if (ctx.runId) extra.run_id = ctx.runId;
|
|
548
|
+
if (ctx.url) extra.url = ctx.url;
|
|
549
|
+
printJsonError(err, extra);
|
|
550
|
+
} else if (options.output === "junit") {
|
|
551
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
552
|
+
console.log(formatJunitError(ctx.name, message, ctx.url));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
421
555
|
function outputResult(result, options) {
|
|
422
556
|
switch (options.output) {
|
|
423
557
|
case "json":
|
|
@@ -443,6 +577,7 @@ async function runSuite(suiteId, options) {
|
|
|
443
577
|
const spinner = createSpinner("Starting suite run...");
|
|
444
578
|
let sessionId;
|
|
445
579
|
let lastQueueEntryId;
|
|
580
|
+
let runId;
|
|
446
581
|
let cleaning = false;
|
|
447
582
|
const cleanup = async (signal) => {
|
|
448
583
|
if (cleaning) process.exit(130);
|
|
@@ -503,6 +638,7 @@ ${signal} received.
|
|
|
503
638
|
metadata: options.metadata
|
|
504
639
|
});
|
|
505
640
|
sessionId = response.session_id;
|
|
641
|
+
runId = response.run_id;
|
|
506
642
|
spinner.stop();
|
|
507
643
|
process.stderr.write(
|
|
508
644
|
`${chalk3.dim("Suite:")} ${suiteId}
|
|
@@ -519,7 +655,8 @@ ${chalk3.dim("Run:")} ${response.run_id}
|
|
|
519
655
|
spinner,
|
|
520
656
|
(entryId) => {
|
|
521
657
|
lastQueueEntryId = entryId;
|
|
522
|
-
}
|
|
658
|
+
},
|
|
659
|
+
{ keepAlive: keepAliveEnabled(options.output) }
|
|
523
660
|
);
|
|
524
661
|
spinner.stop();
|
|
525
662
|
if (process.stdin.isTTY) {
|
|
@@ -530,7 +667,11 @@ ${chalk3.dim("Run:")} ${response.run_id}
|
|
|
530
667
|
process.exit(mapExitCode(result.status));
|
|
531
668
|
} catch (err) {
|
|
532
669
|
spinner.fail(err instanceof Error ? err.message : "Unknown error");
|
|
533
|
-
|
|
670
|
+
outputRunError(err, options, {
|
|
671
|
+
name: suiteId,
|
|
672
|
+
runId,
|
|
673
|
+
url: runId ? `${baseUrl}/tests/runs/${runId}` : void 0
|
|
674
|
+
});
|
|
534
675
|
if (lastQueueEntryId !== void 0) {
|
|
535
676
|
try {
|
|
536
677
|
await client.cancelQueueEntry(lastQueueEntryId);
|
|
@@ -609,7 +750,8 @@ ${signal} received.
|
|
|
609
750
|
model: opts.model,
|
|
610
751
|
provider: opts.provider,
|
|
611
752
|
mode: opts.mode,
|
|
612
|
-
|
|
753
|
+
// Omitted unless asked for: the backend picks tv for each TV suite.
|
|
754
|
+
device_category: opts.device ? void 0 : opts.deviceCategory,
|
|
613
755
|
device_id: opts.device,
|
|
614
756
|
build_id: opts.build,
|
|
615
757
|
metadata: opts.metadata
|
|
@@ -621,11 +763,13 @@ ${signal} received.
|
|
|
621
763
|
const suffix = opts.platform ? ` on platform ${opts.platform}` : "";
|
|
622
764
|
const noMatch = `No suites match tags: ${opts.tag.join(", ")}${suffix}`;
|
|
623
765
|
if (opts.output === "json") printJsonError(noMatch);
|
|
766
|
+
if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", noMatch));
|
|
624
767
|
process.stderr.write(chalk4.yellow(`${noMatch}
|
|
625
768
|
`));
|
|
626
769
|
process.exit(EXIT_INFRA_ERROR);
|
|
627
770
|
}
|
|
628
771
|
if (opts.output === "json") printJsonError(err);
|
|
772
|
+
if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", msg));
|
|
629
773
|
process.stderr.write(chalk4.red(`Failed to start: ${msg}
|
|
630
774
|
`));
|
|
631
775
|
process.exit(EXIT_INFRA_ERROR);
|
|
@@ -657,6 +801,7 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
657
801
|
const deadline = Date.now() + totalTimeout * 1e3;
|
|
658
802
|
const suiteStateBefore = /* @__PURE__ */ new Map();
|
|
659
803
|
const suiteStartedPrinted = /* @__PURE__ */ new Set();
|
|
804
|
+
let lastQueueReason = null;
|
|
660
805
|
const print = (line) => {
|
|
661
806
|
spinner.stop();
|
|
662
807
|
process.stderr.write(line + "\n");
|
|
@@ -664,8 +809,47 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
664
809
|
};
|
|
665
810
|
const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
666
811
|
let last = null;
|
|
812
|
+
const abandon = async (reason) => {
|
|
813
|
+
spinner.stop();
|
|
814
|
+
if (process.stdin.isTTY) {
|
|
815
|
+
process.stdin.setRawMode(false);
|
|
816
|
+
process.stdin.pause();
|
|
817
|
+
}
|
|
818
|
+
process.stderr.write(chalk4.red(`${reason}
|
|
819
|
+
`));
|
|
820
|
+
process.stderr.write("Cancelling multi-suite run...\n");
|
|
821
|
+
try {
|
|
822
|
+
await client.cancelMultiSuiteRun(start.aggregate_id);
|
|
823
|
+
process.stderr.write("Cancellation requested.\n");
|
|
824
|
+
} catch (err) {
|
|
825
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
826
|
+
process.stderr.write(chalk4.yellow(`Could not cancel the run: ${m}
|
|
827
|
+
`));
|
|
828
|
+
}
|
|
829
|
+
if (opts.output === "json") {
|
|
830
|
+
printJsonError(reason, { aggregate_id: start.aggregate_id, result: last });
|
|
831
|
+
} else if (opts.output === "junit") {
|
|
832
|
+
const doc = last ?? {
|
|
833
|
+
id: start.aggregate_id,
|
|
834
|
+
status: "running",
|
|
835
|
+
suite_runs: [],
|
|
836
|
+
started_at: null,
|
|
837
|
+
completed_at: null,
|
|
838
|
+
commit_sha: null
|
|
839
|
+
};
|
|
840
|
+
process.stdout.write(formatMultiSuiteJunit(doc, { timedOut: reason }) + "\n");
|
|
841
|
+
}
|
|
842
|
+
process.exit(EXIT_INFRA_ERROR);
|
|
843
|
+
};
|
|
667
844
|
while (Date.now() < deadline) {
|
|
668
|
-
|
|
845
|
+
try {
|
|
846
|
+
last = await retryTransient(() => client.getMultiSuiteRun(start.aggregate_id), {
|
|
847
|
+
deadline
|
|
848
|
+
});
|
|
849
|
+
} catch (err) {
|
|
850
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
851
|
+
return abandon(`Could not read the run's status: ${m}`);
|
|
852
|
+
}
|
|
669
853
|
let doneCount = 0;
|
|
670
854
|
let queuedChild;
|
|
671
855
|
for (const sr of last.suite_runs) {
|
|
@@ -696,23 +880,31 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
696
880
|
if (queuedChild && queuedChild.queue) {
|
|
697
881
|
const q = queuedChild.queue;
|
|
698
882
|
const target = q.specific_device_id ?? "the device pool";
|
|
883
|
+
const why = queueReasonCopy(q.reason);
|
|
884
|
+
if (why && why !== lastQueueReason) {
|
|
885
|
+
print(
|
|
886
|
+
`${chalk4.yellow("\u23F3")} ${chalk4.bold(queuedChild.suite_name)} is waiting for a device: ${why}`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
lastQueueReason = why;
|
|
699
890
|
spinner.text = `Queued \u2014 ${chalk4.bold(queuedChild.suite_name)} position ${q.position} for ${target} (${doneCount}/${totalMatched} suite(s) complete)`;
|
|
700
891
|
} else {
|
|
892
|
+
lastQueueReason = null;
|
|
701
893
|
spinner.text = `Running suites \u2014 ${doneCount}/${totalMatched} complete...`;
|
|
702
894
|
}
|
|
703
|
-
if (
|
|
895
|
+
if (TERMINAL.has(last.status)) break;
|
|
704
896
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
705
897
|
}
|
|
898
|
+
if (!last || !TERMINAL.has(last.status)) {
|
|
899
|
+
return abandon(
|
|
900
|
+
last ? `Timeout: run did not complete within ${totalTimeout}s` : "Timed out before any status was returned."
|
|
901
|
+
);
|
|
902
|
+
}
|
|
706
903
|
spinner.stop();
|
|
707
904
|
if (process.stdin.isTTY) {
|
|
708
905
|
process.stdin.setRawMode(false);
|
|
709
906
|
process.stdin.pause();
|
|
710
907
|
}
|
|
711
|
-
if (!last) {
|
|
712
|
-
if (opts.output === "json") printJsonError("Timed out before any status was returned.");
|
|
713
|
-
process.stderr.write(chalk4.red("Timed out before any status was returned.\n"));
|
|
714
|
-
process.exit(EXIT_INFRA_ERROR);
|
|
715
|
-
}
|
|
716
908
|
if (opts.output === "json") {
|
|
717
909
|
process.stdout.write(JSON.stringify(last, null, 2) + "\n");
|
|
718
910
|
} else if (opts.output === "junit") {
|
|
@@ -758,6 +950,7 @@ async function runTest(testId, options) {
|
|
|
758
950
|
const client = new ApiClient(baseUrl, token);
|
|
759
951
|
let sessionId;
|
|
760
952
|
let lastQueueEntryId;
|
|
953
|
+
let runId;
|
|
761
954
|
const spinner = createSpinner("Starting test run...");
|
|
762
955
|
let cleaning = false;
|
|
763
956
|
const cleanup = async (signal) => {
|
|
@@ -819,6 +1012,7 @@ ${signal} received.
|
|
|
819
1012
|
metadata: options.metadata
|
|
820
1013
|
});
|
|
821
1014
|
sessionId = response.session_id;
|
|
1015
|
+
runId = response.run_id;
|
|
822
1016
|
spinner.stop();
|
|
823
1017
|
process.stderr.write(
|
|
824
1018
|
`${chalk5.dim("Session:")} ${sessionId || "unknown"}
|
|
@@ -836,7 +1030,8 @@ ${chalk5.dim("Run:")} ${response.run_id}
|
|
|
836
1030
|
spinner,
|
|
837
1031
|
(entryId) => {
|
|
838
1032
|
lastQueueEntryId = entryId;
|
|
839
|
-
}
|
|
1033
|
+
},
|
|
1034
|
+
{ keepAlive: keepAliveEnabled(options.output) }
|
|
840
1035
|
);
|
|
841
1036
|
spinner.stop();
|
|
842
1037
|
if (process.stdin.isTTY) {
|
|
@@ -847,7 +1042,11 @@ ${chalk5.dim("Run:")} ${response.run_id}
|
|
|
847
1042
|
process.exit(mapExitCode(result.status));
|
|
848
1043
|
} catch (err) {
|
|
849
1044
|
spinner.fail(err instanceof Error ? err.message : "Unknown error");
|
|
850
|
-
|
|
1045
|
+
outputRunError(err, options, {
|
|
1046
|
+
name: testId,
|
|
1047
|
+
runId,
|
|
1048
|
+
url: runId ? `${baseUrl}/tests/runs/${runId}` : void 0
|
|
1049
|
+
});
|
|
851
1050
|
if (lastQueueEntryId !== void 0) {
|
|
852
1051
|
try {
|
|
853
1052
|
await client.cancelQueueEntry(lastQueueEntryId);
|
|
@@ -893,7 +1092,9 @@ async function listSuites(options) {
|
|
|
893
1092
|
for (const s of suites) {
|
|
894
1093
|
const platform = s.platform ? chalk6.dim(` [${s.platform}]`) : "";
|
|
895
1094
|
const tagsText = s.tags && s.tags.length > 0 ? chalk6.dim(` [${s.tags.map((t) => t.display_name).join(", ")}]`) : "";
|
|
896
|
-
console.log(
|
|
1095
|
+
console.log(
|
|
1096
|
+
` ${chalk6.cyan(s.id)} ${s.name}${platform}${tagsText} (${s.case_count} ${s.case_count === 1 ? "case" : "cases"})`
|
|
1097
|
+
);
|
|
897
1098
|
}
|
|
898
1099
|
console.log();
|
|
899
1100
|
}
|
|
@@ -1048,9 +1249,12 @@ async function createTest(options) {
|
|
|
1048
1249
|
app_id: appPublicId || "",
|
|
1049
1250
|
steps,
|
|
1050
1251
|
context: options.context,
|
|
1051
|
-
mode: "ai"
|
|
1252
|
+
mode: "ai",
|
|
1253
|
+
if_exists: options.ifExists
|
|
1052
1254
|
});
|
|
1053
|
-
spinner.succeed(
|
|
1255
|
+
spinner.succeed(
|
|
1256
|
+
result.outcome === "updated" ? `Test updated: ${result.name}` : result.outcome === "unchanged" ? `Test already exists, unchanged: ${result.name}` : `Test created: ${result.name}`
|
|
1257
|
+
);
|
|
1054
1258
|
if (options.output === "json") {
|
|
1055
1259
|
console.log(
|
|
1056
1260
|
JSON.stringify(
|
|
@@ -1058,6 +1262,7 @@ async function createTest(options) {
|
|
|
1058
1262
|
test_id: result.public_id,
|
|
1059
1263
|
name: result.name,
|
|
1060
1264
|
app_id: appPublicId || null,
|
|
1265
|
+
outcome: result.outcome ?? null,
|
|
1061
1266
|
url: `${baseUrl}/tests?tab=standalone`
|
|
1062
1267
|
},
|
|
1063
1268
|
null,
|
|
@@ -1078,8 +1283,11 @@ async function createTest(options) {
|
|
|
1078
1283
|
`));
|
|
1079
1284
|
}
|
|
1080
1285
|
} catch (err) {
|
|
1286
|
+
if (options.output === "json") printJsonError(err);
|
|
1081
1287
|
console.error(chalk9.red(err instanceof Error ? err.message : "Failed to create test"));
|
|
1082
|
-
process.exit(
|
|
1288
|
+
process.exit(
|
|
1289
|
+
err instanceof ApiStatusError && err.status === 409 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR
|
|
1290
|
+
);
|
|
1083
1291
|
}
|
|
1084
1292
|
}
|
|
1085
1293
|
|
|
@@ -1161,6 +1369,11 @@ async function logout() {
|
|
|
1161
1369
|
|
|
1162
1370
|
// src/commands/dev-status.ts
|
|
1163
1371
|
import chalk10 from "chalk";
|
|
1372
|
+
function buildLine(info) {
|
|
1373
|
+
if (!info || typeof info.app_build_id !== "number") return null;
|
|
1374
|
+
const version = info.app_version ? ` version ${info.app_version}${info.app_build_number ? ` (${info.app_build_number})` : ""}` : "";
|
|
1375
|
+
return `${info.app_build_id}${version}`;
|
|
1376
|
+
}
|
|
1164
1377
|
async function devStatus(opts) {
|
|
1165
1378
|
const state = readDevState(process.cwd());
|
|
1166
1379
|
if (!state) {
|
|
@@ -1175,12 +1388,14 @@ async function devStatus(opts) {
|
|
|
1175
1388
|
const { token, baseUrl } = resolveAuth(opts);
|
|
1176
1389
|
const api = new ApiClient(baseUrl, token);
|
|
1177
1390
|
let sessionStatus;
|
|
1391
|
+
let info;
|
|
1178
1392
|
try {
|
|
1179
|
-
|
|
1393
|
+
info = await api.getSession(state.session_id);
|
|
1180
1394
|
sessionStatus = info.status;
|
|
1181
1395
|
} catch {
|
|
1182
1396
|
sessionStatus = void 0;
|
|
1183
1397
|
}
|
|
1398
|
+
const build = buildLine(info);
|
|
1184
1399
|
const metro = readMetroState(process.cwd());
|
|
1185
1400
|
if (opts.json) {
|
|
1186
1401
|
console.log(
|
|
@@ -1194,6 +1409,9 @@ async function devStatus(opts) {
|
|
|
1194
1409
|
metro_port: state.metro_port,
|
|
1195
1410
|
started_at: state.started_at,
|
|
1196
1411
|
session_status: sessionStatus ?? null,
|
|
1412
|
+
build_id: info?.app_build_id ?? null,
|
|
1413
|
+
build_version: info?.app_version ?? null,
|
|
1414
|
+
build_number: info?.app_build_number ?? null,
|
|
1197
1415
|
...metro ? { owned_metro_pid: metro.pid, owned_metro_port: metro.port } : {}
|
|
1198
1416
|
})
|
|
1199
1417
|
);
|
|
@@ -1202,6 +1420,7 @@ async function devStatus(opts) {
|
|
|
1202
1420
|
console.error(` session: ${state.session_id}${sessionStatus ? ` (${sessionStatus})` : ""}`);
|
|
1203
1421
|
console.error(` platform: ${state.platform}`);
|
|
1204
1422
|
console.error(` app: ${state.app_id}`);
|
|
1423
|
+
if (build) console.error(` build: ${build}`);
|
|
1205
1424
|
console.error(` url: ${state.page_url}`);
|
|
1206
1425
|
console.error(` started: ${state.started_at}`);
|
|
1207
1426
|
}
|
|
@@ -1353,13 +1572,13 @@ function formatBuildsHuman(builds2) {
|
|
|
1353
1572
|
]);
|
|
1354
1573
|
const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1355
1574
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
1356
|
-
const
|
|
1357
|
-
const lines = [chalk12.bold(
|
|
1575
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
1576
|
+
const lines = [chalk12.bold(join4(padCells(HEADERS)))];
|
|
1358
1577
|
for (const row of rows) {
|
|
1359
1578
|
const padded = padCells(row);
|
|
1360
1579
|
const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
|
|
1361
1580
|
padded[2] = colorFn(row[2]) + padded[2].slice(row[2].length);
|
|
1362
|
-
lines.push(
|
|
1581
|
+
lines.push(join4(padded));
|
|
1363
1582
|
}
|
|
1364
1583
|
return lines.join("\n") + "\n";
|
|
1365
1584
|
}
|
|
@@ -1946,9 +2165,9 @@ function formatVersionsHuman(versions2) {
|
|
|
1946
2165
|
const rows = versions2.map((v) => [v.platform, String(v.build_number), v.updated_at ?? ""]);
|
|
1947
2166
|
const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1948
2167
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
1949
|
-
const
|
|
1950
|
-
const lines = [chalk15.bold(
|
|
1951
|
-
for (const row of rows) lines.push(
|
|
2168
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
2169
|
+
const lines = [chalk15.bold(join4(padCells(HEADERS2)))];
|
|
2170
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
1952
2171
|
return lines.join("\n") + "\n";
|
|
1953
2172
|
}
|
|
1954
2173
|
function printError3(opts, err) {
|
|
@@ -2105,9 +2324,9 @@ function formatEnvVarsHuman(variables) {
|
|
|
2105
2324
|
]);
|
|
2106
2325
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2107
2326
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2108
|
-
const
|
|
2109
|
-
const lines = [chalk17.bold(
|
|
2110
|
-
for (const row of rows) lines.push(
|
|
2327
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
2328
|
+
const lines = [chalk17.bold(join4(padCells(HEADERS3)))];
|
|
2329
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2111
2330
|
return lines.join("\n") + "\n";
|
|
2112
2331
|
}
|
|
2113
2332
|
async function envSet(name, value, opts, deps) {
|
|
@@ -2647,9 +2866,9 @@ function formatAscKeysHuman(keys) {
|
|
|
2647
2866
|
const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, formatUtcTimestamp(k.created_at)]);
|
|
2648
2867
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2649
2868
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2650
|
-
const
|
|
2651
|
-
const lines = [chalk18.bold(
|
|
2652
|
-
for (const row of rows) lines.push(
|
|
2869
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
2870
|
+
const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
|
|
2871
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2653
2872
|
return lines.join("\n") + "\n";
|
|
2654
2873
|
}
|
|
2655
2874
|
async function ascKeyRm(name, opts, deps) {
|
|
@@ -2723,9 +2942,9 @@ function formatPlayKeysHuman(keys) {
|
|
|
2723
2942
|
const rows = keys.map((k) => [k.name, k.client_email, formatUtcTimestamp(k.created_at)]);
|
|
2724
2943
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2725
2944
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2726
|
-
const
|
|
2727
|
-
const lines = [chalk18.bold(
|
|
2728
|
-
for (const row of rows) lines.push(
|
|
2945
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
2946
|
+
const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
|
|
2947
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2729
2948
|
return lines.join("\n") + "\n";
|
|
2730
2949
|
}
|
|
2731
2950
|
async function playKeyRm(name, opts, deps) {
|
|
@@ -2803,9 +3022,9 @@ function formatCredentialsHuman(credentials2) {
|
|
|
2803
3022
|
]);
|
|
2804
3023
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2805
3024
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2806
|
-
const
|
|
2807
|
-
const lines = [chalk18.bold(
|
|
2808
|
-
for (const row of rows) lines.push(
|
|
3025
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
3026
|
+
const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
|
|
3027
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2809
3028
|
return lines.join("\n") + "\n";
|
|
2810
3029
|
}
|
|
2811
3030
|
async function credentialsRm(name, opts, deps) {
|
|
@@ -2922,9 +3141,9 @@ function formatWebhooksHuman(webhooks2) {
|
|
|
2922
3141
|
]);
|
|
2923
3142
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2924
3143
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2925
|
-
const
|
|
2926
|
-
const lines = [chalk19.bold(
|
|
2927
|
-
for (const row of rows) lines.push(
|
|
3144
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
3145
|
+
const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
|
|
3146
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2928
3147
|
return lines.join("\n") + "\n";
|
|
2929
3148
|
}
|
|
2930
3149
|
async function webhooksList(opts, deps) {
|
|
@@ -3199,6 +3418,72 @@ Suggested line for your project's ${chalk20.bold("CLAUDE.md")} (or ${chalk20.bol
|
|
|
3199
3418
|
);
|
|
3200
3419
|
}
|
|
3201
3420
|
|
|
3421
|
+
// src/lib/update-check.ts
|
|
3422
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
3423
|
+
import { homedir as homedir2 } from "os";
|
|
3424
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
3425
|
+
var CACHE_FILE = join3(homedir2(), ".vibeview", "update-check.json");
|
|
3426
|
+
var REGISTRY_URL = "https://registry.npmjs.org/vibeview/latest";
|
|
3427
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3428
|
+
function isNewerVersion(a, b) {
|
|
3429
|
+
const parse = (v) => /^\d+\.\d+\.\d+$/.test(v) ? v.split(".").map(Number) : null;
|
|
3430
|
+
const x = parse(a);
|
|
3431
|
+
const y = parse(b);
|
|
3432
|
+
if (!x || !y) return false;
|
|
3433
|
+
for (let i = 0; i < 3; i++) {
|
|
3434
|
+
if (x[i] !== y[i]) return x[i] > y[i];
|
|
3435
|
+
}
|
|
3436
|
+
return false;
|
|
3437
|
+
}
|
|
3438
|
+
function updateCheckAllowed(argv, env2, stderrIsTTY) {
|
|
3439
|
+
if (!stderrIsTTY) return false;
|
|
3440
|
+
if (env2.CI || env2.NO_UPDATE_NOTIFIER || env2.VIBEVIEW_NO_UPDATE_CHECK) return false;
|
|
3441
|
+
const args = argv.slice(2);
|
|
3442
|
+
if (args[0] === "mcp") return false;
|
|
3443
|
+
if (args.includes("--json")) return false;
|
|
3444
|
+
const output = args.findIndex((a) => a === "--output" || a.startsWith("--output="));
|
|
3445
|
+
if (output !== -1) {
|
|
3446
|
+
const value = args[output].includes("=") ? args[output].split("=")[1] : args[output + 1];
|
|
3447
|
+
if (value && value !== "human") return false;
|
|
3448
|
+
}
|
|
3449
|
+
return true;
|
|
3450
|
+
}
|
|
3451
|
+
function updateNotice(current, latest) {
|
|
3452
|
+
return `A newer vibeview is available: ${current} \u2192 ${latest}. Run \`npm i -g vibeview@latest\`, or \`npx vibeview@latest ...\`.`;
|
|
3453
|
+
}
|
|
3454
|
+
function readCache(file) {
|
|
3455
|
+
try {
|
|
3456
|
+
const parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
3457
|
+
if (typeof parsed?.checked_at === "number" && typeof parsed?.latest === "string") {
|
|
3458
|
+
return parsed;
|
|
3459
|
+
}
|
|
3460
|
+
} catch {
|
|
3461
|
+
}
|
|
3462
|
+
return null;
|
|
3463
|
+
}
|
|
3464
|
+
function checkForUpdate(current, opts) {
|
|
3465
|
+
const file = opts.cacheFile ?? CACHE_FILE;
|
|
3466
|
+
const now = opts.now ?? Date.now();
|
|
3467
|
+
const cache = readCache(file);
|
|
3468
|
+
if (cache && isNewerVersion(cache.latest, current)) {
|
|
3469
|
+
opts.print(updateNotice(current, cache.latest));
|
|
3470
|
+
}
|
|
3471
|
+
if (cache && now - cache.checked_at < ONE_DAY_MS) return Promise.resolve();
|
|
3472
|
+
const fetchLatest = opts.fetchLatest ?? fetchLatestFromRegistry;
|
|
3473
|
+
return fetchLatest().then((latest) => {
|
|
3474
|
+
if (!latest) return;
|
|
3475
|
+
mkdirSync3(dirname2(file), { recursive: true });
|
|
3476
|
+
writeFileSync(file, JSON.stringify({ checked_at: now, latest }) + "\n");
|
|
3477
|
+
}).catch(() => {
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
async function fetchLatestFromRegistry() {
|
|
3481
|
+
const resp = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(1500) });
|
|
3482
|
+
if (!resp.ok) return null;
|
|
3483
|
+
const body = await resp.json();
|
|
3484
|
+
return typeof body.version === "string" ? body.version : null;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3202
3487
|
// src/cli.ts
|
|
3203
3488
|
var require2 = createRequire(import.meta.url);
|
|
3204
3489
|
var pkg = require2("../package.json");
|
|
@@ -3239,10 +3524,12 @@ function resolveMetadata(opts) {
|
|
|
3239
3524
|
}
|
|
3240
3525
|
}
|
|
3241
3526
|
var program = new Command();
|
|
3242
|
-
program.name("vibeview").description(
|
|
3527
|
+
program.name("vibeview").description(
|
|
3528
|
+
"VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku (beta) devices: a React Native dev loop (dev), device control for you or your coding agent (ui-tree, tap, mcp), and tests from CI (run-test, run-suites)"
|
|
3529
|
+
).version(pkg.version);
|
|
3243
3530
|
program.command("run-suite <suite-id>").description("Run all test cases in a suite").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-case breakdown", false).option("--timeout <seconds>", "Max wait time in seconds (queue wait plus execution)", "1800").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
|
|
3244
3531
|
"--device-category <category>",
|
|
3245
|
-
"Device category: phone, tablet, tv (default:
|
|
3532
|
+
"Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
|
|
3246
3533
|
parseDeviceCategory
|
|
3247
3534
|
).option(
|
|
3248
3535
|
"--device <device_id>",
|
|
@@ -3270,7 +3557,7 @@ program.command("run-suites").description("Run all suites matching a tag filter"
|
|
|
3270
3557
|
"Filter matched suites to one platform: ios, android, tvos, androidtv (default: no filter)"
|
|
3271
3558
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-suite progress", false).option("--timeout <seconds>", "Per-suite timeout, capped at 2 hours total", "1800").option("--commit-sha <sha>", "Git commit SHA propagated to every SuiteRun").option("--model <model>", "AI model").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
|
|
3272
3559
|
"--device-category <category>",
|
|
3273
|
-
"Device category: phone, tablet, tv (default:
|
|
3560
|
+
"Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
|
|
3274
3561
|
parseDeviceCategory
|
|
3275
3562
|
).option(
|
|
3276
3563
|
"--device <device_id>",
|
|
@@ -3290,7 +3577,7 @@ program.command("run-suites").description("Run all suites matching a tag filter"
|
|
|
3290
3577
|
});
|
|
3291
3578
|
program.command("run-test <test-id>").description("Run a single test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show step details", false).option("--timeout <seconds>", "Max wait time in seconds (queue wait plus execution)", "1800").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
|
|
3292
3579
|
"--device-category <category>",
|
|
3293
|
-
"Device category: phone, tablet, tv (default:
|
|
3580
|
+
"Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
|
|
3294
3581
|
parseDeviceCategory
|
|
3295
3582
|
).option(
|
|
3296
3583
|
"--device <device_id>",
|
|
@@ -3335,7 +3622,12 @@ program.command("list-devices").description("List devices registered with the pl
|
|
|
3335
3622
|
program.command("upload-app <file>").description(
|
|
3336
3623
|
"Upload an app build (zipped simulator .app for iOS/tvOS, .apk for Android/Android TV, or a Roku channel .zip)"
|
|
3337
3624
|
).option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--name <name>", "App name (auto-detected if not provided)").option("--note <text>", "Note stored on the uploaded build (shown in the dashboard)").action((file, opts) => uploadApp(file, opts));
|
|
3338
|
-
program.command("create-test [name]").description("Create a standalone AI test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--app <app>", "App ID (public_id) or package name (requires --platform)").option("--platform <platform>", "Platform (ios, android) when using package name for --app").option("--steps <json>", "Test steps as inline JSON array").option("--steps-file <path>", "Path to JSON file containing test steps").option("--context <text>", "Additional context for AI test execution").option("--test-name <name>", "Test case name (alternative to positional arg)").
|
|
3625
|
+
program.command("create-test [name]").description("Create a standalone AI test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--app <app>", "App ID (public_id) or package name (requires --platform)").option("--platform <platform>", "Platform (ios, android) when using package name for --app").option("--steps <json>", "Test steps as inline JSON array").option("--steps-file <path>", "Path to JSON file containing test steps").option("--context <text>", "Additional context for AI test execution").option("--test-name <name>", "Test case name (alternative to positional arg)").addOption(
|
|
3626
|
+
new Option(
|
|
3627
|
+
"--if-exists <mode>",
|
|
3628
|
+
"When a standalone test with this name already exists for the app: fail, update it, or create a new one alongside it. Default: reuse an identical test, refuse a different one"
|
|
3629
|
+
).choices(["fail", "update", "new"])
|
|
3630
|
+
).action((name, opts) => createTest({ ...opts, testName: name || opts.testName || "CLI Test" }));
|
|
3339
3631
|
program.command("login").description("Log in to VibeView (interactive)").option("--base-url <url>", "VibeView server URL (defaults to https://vibeview.io)").action((opts) => login(opts));
|
|
3340
3632
|
program.command("logout").description("Revoke the saved login token and remove it from ~/.vibeview/config.json").action(() => logout());
|
|
3341
3633
|
program.command("dev").description(
|
|
@@ -3501,7 +3793,7 @@ program.command("dev-reload").description(
|
|
|
3501
3793
|
program.command("mcp").description(
|
|
3502
3794
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
|
|
3503
3795
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
3504
|
-
const { runMcpServer } = await import("./server-
|
|
3796
|
+
const { runMcpServer } = await import("./server-BGQJOVSN.js");
|
|
3505
3797
|
await runMcpServer({ ...opts, version: pkg.version });
|
|
3506
3798
|
});
|
|
3507
3799
|
program.command("agent-setup").description(
|
|
@@ -3525,6 +3817,10 @@ ${pressButtonsHelp()}`);
|
|
|
3525
3817
|
return runAgentVerb(cmd, positionals, opts);
|
|
3526
3818
|
});
|
|
3527
3819
|
}
|
|
3820
|
+
if (updateCheckAllowed(process.argv, process.env, process.stderr.isTTY)) {
|
|
3821
|
+
void checkForUpdate(pkg.version, { print: (line) => process.stderr.write(`${line}
|
|
3822
|
+
`) });
|
|
3823
|
+
}
|
|
3528
3824
|
program.parseAsync().catch((e) => {
|
|
3529
3825
|
if (e instanceof Error && e.name === "ExitPromptError") {
|
|
3530
3826
|
process.exit(130);
|