vibeview 0.27.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-FTTWLCTE.js → chunk-2JLTC67A.js} +115 -11
- package/dist/{chunk-7IOGMM5W.js → chunk-OKOAWGWV.js} +135 -117
- package/dist/cli.js +364 -86
- package/dist/{server-ZIUZ7U5T.js → server-6SRFOQ6B.js} +298 -53
- package/dist/{upload-app-MDKTSG4O.js → upload-app-WROTRZCA.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-OKOAWGWV.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,
|
|
@@ -40,7 +42,7 @@ import {
|
|
|
40
42
|
resolveAuth,
|
|
41
43
|
saveConfig,
|
|
42
44
|
uploadApp
|
|
43
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-2JLTC67A.js";
|
|
44
46
|
|
|
45
47
|
// src/cli.ts
|
|
46
48
|
import { createRequire } from "module";
|
|
@@ -94,11 +96,8 @@ function formatHuman(result, verbose) {
|
|
|
94
96
|
const icon = result.status === "passed" ? chalk.green("PASS") : chalk.red("FAIL");
|
|
95
97
|
const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
96
98
|
lines.push("");
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} else {
|
|
100
|
-
lines.push(` ${icon} ${chalk.bold(result.run_id)} ${chalk.dim(duration)}`);
|
|
101
|
-
}
|
|
99
|
+
const title = result.suite_name || result.case_name || result.run_id;
|
|
100
|
+
lines.push(` ${icon} ${chalk.bold(title)} ${chalk.dim(duration)}`);
|
|
102
101
|
const cases = resultCases(result);
|
|
103
102
|
const passed = cases.filter((t) => t.status === "passed").length;
|
|
104
103
|
const total = cases.length;
|
|
@@ -123,7 +122,7 @@ function formatHuman(result, verbose) {
|
|
|
123
122
|
}
|
|
124
123
|
if (result.steps && result.steps.length > 0) {
|
|
125
124
|
lines.push("");
|
|
126
|
-
if (result.case_name) {
|
|
125
|
+
if (result.case_name && result.case_name !== title) {
|
|
127
126
|
lines.push(` ${chalk.bold(result.case_name)}`);
|
|
128
127
|
}
|
|
129
128
|
for (const step of result.steps) {
|
|
@@ -173,31 +172,64 @@ function formatJunit(result) {
|
|
|
173
172
|
lines.push(
|
|
174
173
|
` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}" timestamp="${timestamp}">`
|
|
175
174
|
);
|
|
175
|
+
if (result.url) {
|
|
176
|
+
lines.push(" <properties>");
|
|
177
|
+
lines.push(` <property name="vibeview.run_url" value="${escapeXml(result.url)}" />`);
|
|
178
|
+
lines.push(" </properties>");
|
|
179
|
+
}
|
|
176
180
|
for (const tr of cases) {
|
|
177
181
|
const caseName = escapeXml(tr.case_name);
|
|
178
182
|
const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
|
|
179
|
-
|
|
183
|
+
const out = tr.url ? ` <system-out>${escapeXml(tr.url)}</system-out>` : null;
|
|
184
|
+
if (tr.status === "passed" && !out) {
|
|
180
185
|
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
|
|
181
|
-
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
189
|
+
if (tr.status === "failed") {
|
|
182
190
|
const msg = escapeXml(tr.error_message || "Test failed");
|
|
183
|
-
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
184
191
|
lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
|
|
185
|
-
|
|
186
|
-
} else {
|
|
192
|
+
} else if (tr.status !== "passed") {
|
|
187
193
|
const msg = escapeXml(tr.error_message || "Infrastructure error");
|
|
188
|
-
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
189
194
|
lines.push(` <error message="${msg}" type="InfraError">${msg}</error>`);
|
|
190
|
-
lines.push(" </testcase>");
|
|
191
195
|
}
|
|
196
|
+
if (out) lines.push(out);
|
|
197
|
+
lines.push(" </testcase>");
|
|
192
198
|
}
|
|
193
199
|
lines.push(" </testsuite>");
|
|
194
200
|
lines.push("</testsuites>");
|
|
195
201
|
return lines.join("\n");
|
|
196
202
|
}
|
|
197
|
-
function
|
|
203
|
+
function formatJunitError(name, message, url) {
|
|
204
|
+
const n = escapeXml(name);
|
|
205
|
+
const msg = escapeXml(message);
|
|
206
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
207
|
+
const lines = [];
|
|
208
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
209
|
+
lines.push(`<testsuites name="vibeview" tests="1" failures="0" errors="1" time="0">`);
|
|
210
|
+
lines.push(
|
|
211
|
+
` <testsuite name="${n}" tests="1" failures="0" errors="1" time="0" timestamp="${timestamp}">`
|
|
212
|
+
);
|
|
213
|
+
if (url) {
|
|
214
|
+
lines.push(" <properties>");
|
|
215
|
+
lines.push(` <property name="vibeview.run_url" value="${escapeXml(url)}" />`);
|
|
216
|
+
lines.push(" </properties>");
|
|
217
|
+
}
|
|
218
|
+
lines.push(` <testcase name="${n}" classname="${n}" time="0">`);
|
|
219
|
+
lines.push(` <error message="${msg}" type="InfraError">${msg}</error>`);
|
|
220
|
+
lines.push(" </testcase>");
|
|
221
|
+
lines.push(" </testsuite>");
|
|
222
|
+
lines.push("</testsuites>");
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
|
225
|
+
var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
226
|
+
function formatMultiSuiteJunit(result, opts = {}) {
|
|
227
|
+
const unfinished = (status) => opts.timedOut !== void 0 && !MULTI_TERMINAL.has(status);
|
|
228
|
+
const errored = (status) => status === "error" || unfinished(status);
|
|
198
229
|
const tests = result.suite_runs.reduce((sum, sr) => sum + sr.passed + sr.failed, 0);
|
|
199
230
|
const failures = result.suite_runs.reduce((sum, sr) => sum + sr.failed, 0);
|
|
200
|
-
const
|
|
231
|
+
const noSuites = opts.timedOut !== void 0 && result.suite_runs.length === 0;
|
|
232
|
+
const totalErrors = result.suite_runs.filter((sr) => errored(sr.status)).length + (noSuites ? 1 : 0);
|
|
201
233
|
const timestamp = result.started_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
202
234
|
const lines = [];
|
|
203
235
|
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
@@ -208,43 +240,113 @@ function formatMultiSuiteJunit(result) {
|
|
|
208
240
|
const suiteName = escapeXml(sr.suite_name || sr.suite_id);
|
|
209
241
|
const suiteTests = sr.passed + sr.failed;
|
|
210
242
|
const suiteFails = sr.failed;
|
|
211
|
-
const suiteErrors = sr.status
|
|
243
|
+
const suiteErrors = errored(sr.status) ? 1 : 0;
|
|
212
244
|
lines.push(
|
|
213
245
|
` <testsuite name="${suiteName}" tests="${suiteTests}" failures="${suiteFails}" errors="${suiteErrors}" timestamp="${timestamp}">`
|
|
214
246
|
);
|
|
215
|
-
if (
|
|
247
|
+
if (sr.url) {
|
|
248
|
+
lines.push(" <properties>");
|
|
249
|
+
lines.push(` <property name="vibeview.run_url" value="${escapeXml(sr.url)}" />`);
|
|
250
|
+
lines.push(" </properties>");
|
|
251
|
+
}
|
|
252
|
+
if (unfinished(sr.status)) {
|
|
253
|
+
const msg = escapeXml(`${opts.timedOut} (suite was ${sr.status})`);
|
|
254
|
+
lines.push(` <error message="${msg}" type="InfraError" />`);
|
|
255
|
+
} else if (suiteErrors > 0) {
|
|
216
256
|
lines.push(` <error message="Suite errored" type="InfraError" />`);
|
|
217
257
|
}
|
|
218
258
|
lines.push(" </testsuite>");
|
|
219
259
|
}
|
|
260
|
+
if (noSuites) {
|
|
261
|
+
const msg = escapeXml(opts.timedOut);
|
|
262
|
+
lines.push(
|
|
263
|
+
` <testsuite name="vibeview run-suites" tests="0" failures="0" errors="1" timestamp="${timestamp}">`
|
|
264
|
+
);
|
|
265
|
+
lines.push(` <error message="${msg}" type="InfraError" />`);
|
|
266
|
+
lines.push(" </testsuite>");
|
|
267
|
+
}
|
|
220
268
|
lines.push("</testsuites>");
|
|
221
269
|
return lines.join("\n");
|
|
222
270
|
}
|
|
223
271
|
|
|
224
272
|
// src/lib/poll.ts
|
|
225
|
-
function
|
|
226
|
-
|
|
273
|
+
function resumeSpinner(spinner, text) {
|
|
274
|
+
if (spinner.isEnabled === false) {
|
|
275
|
+
if (text !== void 0) spinner.text = text;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
spinner.start(text);
|
|
279
|
+
}
|
|
280
|
+
function printBelowSpinner(spinner, line, nextText) {
|
|
281
|
+
const text = nextText ?? spinner.text;
|
|
227
282
|
spinner.stop();
|
|
228
283
|
process.stderr.write(line + "\n");
|
|
229
|
-
spinner
|
|
284
|
+
resumeSpinner(spinner, text);
|
|
285
|
+
}
|
|
286
|
+
function printStepLines(stepNum, total, status, text, actions, error) {
|
|
287
|
+
const icon = status === "passed" ? chalk2.green("\u2713") : status === "failed" ? chalk2.red("\u2717") : chalk2.yellow("!");
|
|
288
|
+
process.stderr.write(` ${icon} Step ${stepNum}/${total}: ${text}
|
|
289
|
+
`);
|
|
290
|
+
for (const action of actions ?? []) {
|
|
291
|
+
process.stderr.write(` ${chalk2.dim(formatAction(action))}
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
294
|
+
if (error) process.stderr.write(` ${chalk2.red(error)}
|
|
295
|
+
`);
|
|
296
|
+
}
|
|
297
|
+
var PollTimeoutError = class extends Error {
|
|
298
|
+
constructor(message) {
|
|
299
|
+
super(message);
|
|
300
|
+
this.name = "PollTimeoutError";
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
var POLL_RETRY_DELAYS_MS = [2e3, 4e3, 8e3, 16e3, 3e4];
|
|
304
|
+
function isTransientPollError(err) {
|
|
305
|
+
if (err instanceof NetworkError) return true;
|
|
306
|
+
if (err instanceof ApiStatusError) {
|
|
307
|
+
return err.status === 408 || err.status === 429 || err.status >= 500;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
async function retryTransient(fn, opts = {}) {
|
|
312
|
+
for (let attempt = 0; ; attempt++) {
|
|
313
|
+
try {
|
|
314
|
+
return await fn();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const delay = POLL_RETRY_DELAYS_MS[attempt];
|
|
317
|
+
if (!isTransientPollError(err) || delay === void 0) throw err;
|
|
318
|
+
if (opts.deadline !== void 0 && Date.now() + delay >= opts.deadline) throw err;
|
|
319
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
320
|
+
process.stderr.write(
|
|
321
|
+
`${opts.label ?? "Status poll"} failed (${message}); retrying in ${delay / 1e3}s (${attempt + 1}/${POLL_RETRY_DELAYS_MS.length})
|
|
322
|
+
`
|
|
323
|
+
);
|
|
324
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
230
327
|
}
|
|
231
|
-
|
|
328
|
+
function keepAliveEnabled(output) {
|
|
329
|
+
return output !== "json" && output !== "junit";
|
|
330
|
+
}
|
|
331
|
+
async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObserved, opts = {}) {
|
|
232
332
|
const deadline = Date.now() + timeoutMs;
|
|
233
333
|
const isCI = process.env.CI === "true" || process.env.CI === "1" || !process.stdout.isTTY;
|
|
334
|
+
const keepAlive = isCI && opts.keepAlive !== false;
|
|
234
335
|
let lastLogTime = Date.now();
|
|
235
336
|
let lastSeenEventCount = 0;
|
|
236
337
|
let totalSteps;
|
|
237
338
|
let currentStepLabel = "";
|
|
339
|
+
const printedSteps = /* @__PURE__ */ new Set();
|
|
238
340
|
let cachedDeviceLabel = null;
|
|
239
341
|
const caseStateBefore = /* @__PURE__ */ new Map();
|
|
240
342
|
const caseStartedPrinted = /* @__PURE__ */ new Set();
|
|
241
343
|
while (Date.now() < deadline) {
|
|
242
|
-
const result = await client.getRunStatus(runId);
|
|
344
|
+
const result = await retryTransient(() => client.getRunStatus(runId), { deadline });
|
|
243
345
|
const progress = result.progress || [];
|
|
244
346
|
if (progress.length > lastSeenEventCount) {
|
|
245
347
|
const newEvents = progress.slice(lastSeenEventCount);
|
|
246
348
|
for (const evt of newEvents) {
|
|
247
|
-
const ctx = { spinner, totalSteps, currentStepLabel };
|
|
349
|
+
const ctx = { spinner, totalSteps, currentStepLabel, printedSteps };
|
|
248
350
|
displayProgressEvent(evt, ctx);
|
|
249
351
|
totalSteps = ctx.totalSteps;
|
|
250
352
|
currentStepLabel = ctx.currentStepLabel;
|
|
@@ -277,6 +379,20 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
277
379
|
}
|
|
278
380
|
}
|
|
279
381
|
if (["passed", "failed", "error", "cancelled"].includes(result.status)) {
|
|
382
|
+
if (printedSteps.size > 0 && Array.isArray(result.steps)) {
|
|
383
|
+
spinner.stop();
|
|
384
|
+
result.steps.forEach((st, i) => {
|
|
385
|
+
if (printedSteps.has(i)) return;
|
|
386
|
+
printStepLines(
|
|
387
|
+
i + 1,
|
|
388
|
+
totalSteps ?? result.steps.length,
|
|
389
|
+
st.status,
|
|
390
|
+
st.step,
|
|
391
|
+
st.actions,
|
|
392
|
+
st.error
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
280
396
|
return result;
|
|
281
397
|
}
|
|
282
398
|
if (result.status === "queued" && result.queue) {
|
|
@@ -317,20 +433,24 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
317
433
|
spinner.text = `Status: ${result.status}...`;
|
|
318
434
|
}
|
|
319
435
|
}
|
|
320
|
-
if (
|
|
321
|
-
|
|
436
|
+
if (keepAlive && Date.now() - lastLogTime >= 3e4) {
|
|
437
|
+
const where = result.status === "queued" && result.queue ? ` (position ${result.queue.position} in line for a device)` : "";
|
|
438
|
+
process.stderr.write(`Status: ${result.status}${where}...
|
|
439
|
+
`);
|
|
322
440
|
lastLogTime = Date.now();
|
|
323
441
|
}
|
|
324
442
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
325
443
|
}
|
|
326
|
-
throw new
|
|
444
|
+
throw new PollTimeoutError(
|
|
445
|
+
`Timeout: run did not complete within ${Math.round(timeoutMs / 1e3)}s`
|
|
446
|
+
);
|
|
327
447
|
}
|
|
328
448
|
function formatAction(action) {
|
|
329
449
|
const tool = action.tool;
|
|
330
450
|
const args = action.args || {};
|
|
331
451
|
switch (tool) {
|
|
332
452
|
case "tap":
|
|
333
|
-
return `tap ${args.element_ref
|
|
453
|
+
return `tap ${args.ref ?? args.element_ref ?? ""}`.trimEnd();
|
|
334
454
|
case "tap_coordinates":
|
|
335
455
|
return `tap (${args.x}, ${args.y})`;
|
|
336
456
|
case "type_text":
|
|
@@ -358,7 +478,8 @@ function displayProgressEvent(evt, ctx) {
|
|
|
358
478
|
case "run_started":
|
|
359
479
|
printBelowSpinner(
|
|
360
480
|
spinner,
|
|
361
|
-
`${chalk2.cyan("\u25B6")} AI test started \u2014 ${evt.total_steps} step(s), model: ${chalk2.bold(evt.model || "default")}
|
|
481
|
+
`${chalk2.cyan("\u25B6")} AI test started \u2014 ${evt.total_steps} step(s), model: ${chalk2.bold(evt.model || "default")}`,
|
|
482
|
+
" Starting test..."
|
|
362
483
|
);
|
|
363
484
|
ctx.totalSteps = evt.total_steps ?? ctx.totalSteps;
|
|
364
485
|
break;
|
|
@@ -370,23 +491,17 @@ function displayProgressEvent(evt, ctx) {
|
|
|
370
491
|
break;
|
|
371
492
|
}
|
|
372
493
|
case "step_completed": {
|
|
373
|
-
const
|
|
494
|
+
const stepIndex = evt.step_index ?? 0;
|
|
495
|
+
const stepNum = stepIndex + 1;
|
|
374
496
|
const total = ctx.totalSteps ?? evt.total_steps ?? "?";
|
|
375
|
-
|
|
497
|
+
if (ctx.printedSteps.has(stepIndex)) break;
|
|
498
|
+
ctx.printedSteps.add(stepIndex);
|
|
376
499
|
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();
|
|
500
|
+
printStepLines(stepNum, total, evt.status, evt.step_text || "", evt.actions, evt.error);
|
|
501
|
+
resumeSpinner(
|
|
502
|
+
spinner,
|
|
503
|
+
typeof total === "number" && stepNum >= total ? " Finishing..." : " Next step..."
|
|
504
|
+
);
|
|
390
505
|
break;
|
|
391
506
|
}
|
|
392
507
|
case "conversation_turn": {
|
|
@@ -405,7 +520,7 @@ function displayProgressEvent(evt, ctx) {
|
|
|
405
520
|
`${icon} Run ${evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
|
|
406
521
|
`
|
|
407
522
|
);
|
|
408
|
-
spinner
|
|
523
|
+
resumeSpinner(spinner);
|
|
409
524
|
break;
|
|
410
525
|
}
|
|
411
526
|
case "app_installing":
|
|
@@ -418,6 +533,17 @@ function displayProgressEvent(evt, ctx) {
|
|
|
418
533
|
break;
|
|
419
534
|
}
|
|
420
535
|
}
|
|
536
|
+
function outputRunError(err, options, ctx) {
|
|
537
|
+
if (options.output === "json") {
|
|
538
|
+
const extra = {};
|
|
539
|
+
if (ctx.runId) extra.run_id = ctx.runId;
|
|
540
|
+
if (ctx.url) extra.url = ctx.url;
|
|
541
|
+
printJsonError(err, extra);
|
|
542
|
+
} else if (options.output === "junit") {
|
|
543
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
544
|
+
console.log(formatJunitError(ctx.name, message, ctx.url));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
421
547
|
function outputResult(result, options) {
|
|
422
548
|
switch (options.output) {
|
|
423
549
|
case "json":
|
|
@@ -443,6 +569,7 @@ async function runSuite(suiteId, options) {
|
|
|
443
569
|
const spinner = createSpinner("Starting suite run...");
|
|
444
570
|
let sessionId;
|
|
445
571
|
let lastQueueEntryId;
|
|
572
|
+
let runId;
|
|
446
573
|
let cleaning = false;
|
|
447
574
|
const cleanup = async (signal) => {
|
|
448
575
|
if (cleaning) process.exit(130);
|
|
@@ -503,6 +630,7 @@ ${signal} received.
|
|
|
503
630
|
metadata: options.metadata
|
|
504
631
|
});
|
|
505
632
|
sessionId = response.session_id;
|
|
633
|
+
runId = response.run_id;
|
|
506
634
|
spinner.stop();
|
|
507
635
|
process.stderr.write(
|
|
508
636
|
`${chalk3.dim("Suite:")} ${suiteId}
|
|
@@ -519,7 +647,8 @@ ${chalk3.dim("Run:")} ${response.run_id}
|
|
|
519
647
|
spinner,
|
|
520
648
|
(entryId) => {
|
|
521
649
|
lastQueueEntryId = entryId;
|
|
522
|
-
}
|
|
650
|
+
},
|
|
651
|
+
{ keepAlive: keepAliveEnabled(options.output) }
|
|
523
652
|
);
|
|
524
653
|
spinner.stop();
|
|
525
654
|
if (process.stdin.isTTY) {
|
|
@@ -530,7 +659,11 @@ ${chalk3.dim("Run:")} ${response.run_id}
|
|
|
530
659
|
process.exit(mapExitCode(result.status));
|
|
531
660
|
} catch (err) {
|
|
532
661
|
spinner.fail(err instanceof Error ? err.message : "Unknown error");
|
|
533
|
-
|
|
662
|
+
outputRunError(err, options, {
|
|
663
|
+
name: suiteId,
|
|
664
|
+
runId,
|
|
665
|
+
url: runId ? `${baseUrl}/tests/runs/${runId}` : void 0
|
|
666
|
+
});
|
|
534
667
|
if (lastQueueEntryId !== void 0) {
|
|
535
668
|
try {
|
|
536
669
|
await client.cancelQueueEntry(lastQueueEntryId);
|
|
@@ -621,11 +754,13 @@ ${signal} received.
|
|
|
621
754
|
const suffix = opts.platform ? ` on platform ${opts.platform}` : "";
|
|
622
755
|
const noMatch = `No suites match tags: ${opts.tag.join(", ")}${suffix}`;
|
|
623
756
|
if (opts.output === "json") printJsonError(noMatch);
|
|
757
|
+
if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", noMatch));
|
|
624
758
|
process.stderr.write(chalk4.yellow(`${noMatch}
|
|
625
759
|
`));
|
|
626
760
|
process.exit(EXIT_INFRA_ERROR);
|
|
627
761
|
}
|
|
628
762
|
if (opts.output === "json") printJsonError(err);
|
|
763
|
+
if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", msg));
|
|
629
764
|
process.stderr.write(chalk4.red(`Failed to start: ${msg}
|
|
630
765
|
`));
|
|
631
766
|
process.exit(EXIT_INFRA_ERROR);
|
|
@@ -664,8 +799,47 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
664
799
|
};
|
|
665
800
|
const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
666
801
|
let last = null;
|
|
802
|
+
const abandon = async (reason) => {
|
|
803
|
+
spinner.stop();
|
|
804
|
+
if (process.stdin.isTTY) {
|
|
805
|
+
process.stdin.setRawMode(false);
|
|
806
|
+
process.stdin.pause();
|
|
807
|
+
}
|
|
808
|
+
process.stderr.write(chalk4.red(`${reason}
|
|
809
|
+
`));
|
|
810
|
+
process.stderr.write("Cancelling multi-suite run...\n");
|
|
811
|
+
try {
|
|
812
|
+
await client.cancelMultiSuiteRun(start.aggregate_id);
|
|
813
|
+
process.stderr.write("Cancellation requested.\n");
|
|
814
|
+
} catch (err) {
|
|
815
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
816
|
+
process.stderr.write(chalk4.yellow(`Could not cancel the run: ${m}
|
|
817
|
+
`));
|
|
818
|
+
}
|
|
819
|
+
if (opts.output === "json") {
|
|
820
|
+
printJsonError(reason, { aggregate_id: start.aggregate_id, result: last });
|
|
821
|
+
} else if (opts.output === "junit") {
|
|
822
|
+
const doc = last ?? {
|
|
823
|
+
id: start.aggregate_id,
|
|
824
|
+
status: "running",
|
|
825
|
+
suite_runs: [],
|
|
826
|
+
started_at: null,
|
|
827
|
+
completed_at: null,
|
|
828
|
+
commit_sha: null
|
|
829
|
+
};
|
|
830
|
+
process.stdout.write(formatMultiSuiteJunit(doc, { timedOut: reason }) + "\n");
|
|
831
|
+
}
|
|
832
|
+
process.exit(EXIT_INFRA_ERROR);
|
|
833
|
+
};
|
|
667
834
|
while (Date.now() < deadline) {
|
|
668
|
-
|
|
835
|
+
try {
|
|
836
|
+
last = await retryTransient(() => client.getMultiSuiteRun(start.aggregate_id), {
|
|
837
|
+
deadline
|
|
838
|
+
});
|
|
839
|
+
} catch (err) {
|
|
840
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
841
|
+
return abandon(`Could not read the run's status: ${m}`);
|
|
842
|
+
}
|
|
669
843
|
let doneCount = 0;
|
|
670
844
|
let queuedChild;
|
|
671
845
|
for (const sr of last.suite_runs) {
|
|
@@ -700,19 +874,19 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
700
874
|
} else {
|
|
701
875
|
spinner.text = `Running suites \u2014 ${doneCount}/${totalMatched} complete...`;
|
|
702
876
|
}
|
|
703
|
-
if (
|
|
877
|
+
if (TERMINAL.has(last.status)) break;
|
|
704
878
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
705
879
|
}
|
|
880
|
+
if (!last || !TERMINAL.has(last.status)) {
|
|
881
|
+
return abandon(
|
|
882
|
+
last ? `Timeout: run did not complete within ${totalTimeout}s` : "Timed out before any status was returned."
|
|
883
|
+
);
|
|
884
|
+
}
|
|
706
885
|
spinner.stop();
|
|
707
886
|
if (process.stdin.isTTY) {
|
|
708
887
|
process.stdin.setRawMode(false);
|
|
709
888
|
process.stdin.pause();
|
|
710
889
|
}
|
|
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
890
|
if (opts.output === "json") {
|
|
717
891
|
process.stdout.write(JSON.stringify(last, null, 2) + "\n");
|
|
718
892
|
} else if (opts.output === "junit") {
|
|
@@ -758,6 +932,7 @@ async function runTest(testId, options) {
|
|
|
758
932
|
const client = new ApiClient(baseUrl, token);
|
|
759
933
|
let sessionId;
|
|
760
934
|
let lastQueueEntryId;
|
|
935
|
+
let runId;
|
|
761
936
|
const spinner = createSpinner("Starting test run...");
|
|
762
937
|
let cleaning = false;
|
|
763
938
|
const cleanup = async (signal) => {
|
|
@@ -819,6 +994,7 @@ ${signal} received.
|
|
|
819
994
|
metadata: options.metadata
|
|
820
995
|
});
|
|
821
996
|
sessionId = response.session_id;
|
|
997
|
+
runId = response.run_id;
|
|
822
998
|
spinner.stop();
|
|
823
999
|
process.stderr.write(
|
|
824
1000
|
`${chalk5.dim("Session:")} ${sessionId || "unknown"}
|
|
@@ -836,7 +1012,8 @@ ${chalk5.dim("Run:")} ${response.run_id}
|
|
|
836
1012
|
spinner,
|
|
837
1013
|
(entryId) => {
|
|
838
1014
|
lastQueueEntryId = entryId;
|
|
839
|
-
}
|
|
1015
|
+
},
|
|
1016
|
+
{ keepAlive: keepAliveEnabled(options.output) }
|
|
840
1017
|
);
|
|
841
1018
|
spinner.stop();
|
|
842
1019
|
if (process.stdin.isTTY) {
|
|
@@ -847,7 +1024,11 @@ ${chalk5.dim("Run:")} ${response.run_id}
|
|
|
847
1024
|
process.exit(mapExitCode(result.status));
|
|
848
1025
|
} catch (err) {
|
|
849
1026
|
spinner.fail(err instanceof Error ? err.message : "Unknown error");
|
|
850
|
-
|
|
1027
|
+
outputRunError(err, options, {
|
|
1028
|
+
name: testId,
|
|
1029
|
+
runId,
|
|
1030
|
+
url: runId ? `${baseUrl}/tests/runs/${runId}` : void 0
|
|
1031
|
+
});
|
|
851
1032
|
if (lastQueueEntryId !== void 0) {
|
|
852
1033
|
try {
|
|
853
1034
|
await client.cancelQueueEntry(lastQueueEntryId);
|
|
@@ -893,7 +1074,9 @@ async function listSuites(options) {
|
|
|
893
1074
|
for (const s of suites) {
|
|
894
1075
|
const platform = s.platform ? chalk6.dim(` [${s.platform}]`) : "";
|
|
895
1076
|
const tagsText = s.tags && s.tags.length > 0 ? chalk6.dim(` [${s.tags.map((t) => t.display_name).join(", ")}]`) : "";
|
|
896
|
-
console.log(
|
|
1077
|
+
console.log(
|
|
1078
|
+
` ${chalk6.cyan(s.id)} ${s.name}${platform}${tagsText} (${s.case_count} ${s.case_count === 1 ? "case" : "cases"})`
|
|
1079
|
+
);
|
|
897
1080
|
}
|
|
898
1081
|
console.log();
|
|
899
1082
|
}
|
|
@@ -1048,9 +1231,12 @@ async function createTest(options) {
|
|
|
1048
1231
|
app_id: appPublicId || "",
|
|
1049
1232
|
steps,
|
|
1050
1233
|
context: options.context,
|
|
1051
|
-
mode: "ai"
|
|
1234
|
+
mode: "ai",
|
|
1235
|
+
if_exists: options.ifExists
|
|
1052
1236
|
});
|
|
1053
|
-
spinner.succeed(
|
|
1237
|
+
spinner.succeed(
|
|
1238
|
+
result.outcome === "updated" ? `Test updated: ${result.name}` : result.outcome === "unchanged" ? `Test already exists, unchanged: ${result.name}` : `Test created: ${result.name}`
|
|
1239
|
+
);
|
|
1054
1240
|
if (options.output === "json") {
|
|
1055
1241
|
console.log(
|
|
1056
1242
|
JSON.stringify(
|
|
@@ -1058,6 +1244,7 @@ async function createTest(options) {
|
|
|
1058
1244
|
test_id: result.public_id,
|
|
1059
1245
|
name: result.name,
|
|
1060
1246
|
app_id: appPublicId || null,
|
|
1247
|
+
outcome: result.outcome ?? null,
|
|
1061
1248
|
url: `${baseUrl}/tests?tab=standalone`
|
|
1062
1249
|
},
|
|
1063
1250
|
null,
|
|
@@ -1078,8 +1265,11 @@ async function createTest(options) {
|
|
|
1078
1265
|
`));
|
|
1079
1266
|
}
|
|
1080
1267
|
} catch (err) {
|
|
1268
|
+
if (options.output === "json") printJsonError(err);
|
|
1081
1269
|
console.error(chalk9.red(err instanceof Error ? err.message : "Failed to create test"));
|
|
1082
|
-
process.exit(
|
|
1270
|
+
process.exit(
|
|
1271
|
+
err instanceof ApiStatusError && err.status === 409 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR
|
|
1272
|
+
);
|
|
1083
1273
|
}
|
|
1084
1274
|
}
|
|
1085
1275
|
|
|
@@ -1161,6 +1351,11 @@ async function logout() {
|
|
|
1161
1351
|
|
|
1162
1352
|
// src/commands/dev-status.ts
|
|
1163
1353
|
import chalk10 from "chalk";
|
|
1354
|
+
function buildLine(info) {
|
|
1355
|
+
if (!info || typeof info.app_build_id !== "number") return null;
|
|
1356
|
+
const version = info.app_version ? ` version ${info.app_version}${info.app_build_number ? ` (${info.app_build_number})` : ""}` : "";
|
|
1357
|
+
return `${info.app_build_id}${version}`;
|
|
1358
|
+
}
|
|
1164
1359
|
async function devStatus(opts) {
|
|
1165
1360
|
const state = readDevState(process.cwd());
|
|
1166
1361
|
if (!state) {
|
|
@@ -1175,12 +1370,14 @@ async function devStatus(opts) {
|
|
|
1175
1370
|
const { token, baseUrl } = resolveAuth(opts);
|
|
1176
1371
|
const api = new ApiClient(baseUrl, token);
|
|
1177
1372
|
let sessionStatus;
|
|
1373
|
+
let info;
|
|
1178
1374
|
try {
|
|
1179
|
-
|
|
1375
|
+
info = await api.getSession(state.session_id);
|
|
1180
1376
|
sessionStatus = info.status;
|
|
1181
1377
|
} catch {
|
|
1182
1378
|
sessionStatus = void 0;
|
|
1183
1379
|
}
|
|
1380
|
+
const build = buildLine(info);
|
|
1184
1381
|
const metro = readMetroState(process.cwd());
|
|
1185
1382
|
if (opts.json) {
|
|
1186
1383
|
console.log(
|
|
@@ -1194,6 +1391,9 @@ async function devStatus(opts) {
|
|
|
1194
1391
|
metro_port: state.metro_port,
|
|
1195
1392
|
started_at: state.started_at,
|
|
1196
1393
|
session_status: sessionStatus ?? null,
|
|
1394
|
+
build_id: info?.app_build_id ?? null,
|
|
1395
|
+
build_version: info?.app_version ?? null,
|
|
1396
|
+
build_number: info?.app_build_number ?? null,
|
|
1197
1397
|
...metro ? { owned_metro_pid: metro.pid, owned_metro_port: metro.port } : {}
|
|
1198
1398
|
})
|
|
1199
1399
|
);
|
|
@@ -1202,6 +1402,7 @@ async function devStatus(opts) {
|
|
|
1202
1402
|
console.error(` session: ${state.session_id}${sessionStatus ? ` (${sessionStatus})` : ""}`);
|
|
1203
1403
|
console.error(` platform: ${state.platform}`);
|
|
1204
1404
|
console.error(` app: ${state.app_id}`);
|
|
1405
|
+
if (build) console.error(` build: ${build}`);
|
|
1205
1406
|
console.error(` url: ${state.page_url}`);
|
|
1206
1407
|
console.error(` started: ${state.started_at}`);
|
|
1207
1408
|
}
|
|
@@ -1353,13 +1554,13 @@ function formatBuildsHuman(builds2) {
|
|
|
1353
1554
|
]);
|
|
1354
1555
|
const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1355
1556
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
1356
|
-
const
|
|
1357
|
-
const lines = [chalk12.bold(
|
|
1557
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
1558
|
+
const lines = [chalk12.bold(join4(padCells(HEADERS)))];
|
|
1358
1559
|
for (const row of rows) {
|
|
1359
1560
|
const padded = padCells(row);
|
|
1360
1561
|
const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
|
|
1361
1562
|
padded[2] = colorFn(row[2]) + padded[2].slice(row[2].length);
|
|
1362
|
-
lines.push(
|
|
1563
|
+
lines.push(join4(padded));
|
|
1363
1564
|
}
|
|
1364
1565
|
return lines.join("\n") + "\n";
|
|
1365
1566
|
}
|
|
@@ -1946,9 +2147,9 @@ function formatVersionsHuman(versions2) {
|
|
|
1946
2147
|
const rows = versions2.map((v) => [v.platform, String(v.build_number), v.updated_at ?? ""]);
|
|
1947
2148
|
const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1948
2149
|
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(
|
|
2150
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
2151
|
+
const lines = [chalk15.bold(join4(padCells(HEADERS2)))];
|
|
2152
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
1952
2153
|
return lines.join("\n") + "\n";
|
|
1953
2154
|
}
|
|
1954
2155
|
function printError3(opts, err) {
|
|
@@ -2105,9 +2306,9 @@ function formatEnvVarsHuman(variables) {
|
|
|
2105
2306
|
]);
|
|
2106
2307
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2107
2308
|
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(
|
|
2309
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
2310
|
+
const lines = [chalk17.bold(join4(padCells(HEADERS3)))];
|
|
2311
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2111
2312
|
return lines.join("\n") + "\n";
|
|
2112
2313
|
}
|
|
2113
2314
|
async function envSet(name, value, opts, deps) {
|
|
@@ -2647,9 +2848,9 @@ function formatAscKeysHuman(keys) {
|
|
|
2647
2848
|
const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, formatUtcTimestamp(k.created_at)]);
|
|
2648
2849
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2649
2850
|
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(
|
|
2851
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
2852
|
+
const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
|
|
2853
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2653
2854
|
return lines.join("\n") + "\n";
|
|
2654
2855
|
}
|
|
2655
2856
|
async function ascKeyRm(name, opts, deps) {
|
|
@@ -2723,9 +2924,9 @@ function formatPlayKeysHuman(keys) {
|
|
|
2723
2924
|
const rows = keys.map((k) => [k.name, k.client_email, formatUtcTimestamp(k.created_at)]);
|
|
2724
2925
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2725
2926
|
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(
|
|
2927
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
2928
|
+
const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
|
|
2929
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2729
2930
|
return lines.join("\n") + "\n";
|
|
2730
2931
|
}
|
|
2731
2932
|
async function playKeyRm(name, opts, deps) {
|
|
@@ -2803,9 +3004,9 @@ function formatCredentialsHuman(credentials2) {
|
|
|
2803
3004
|
]);
|
|
2804
3005
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2805
3006
|
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(
|
|
3007
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
3008
|
+
const lines = [chalk18.bold(join4(padCells(HEADERS3)))];
|
|
3009
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2809
3010
|
return lines.join("\n") + "\n";
|
|
2810
3011
|
}
|
|
2811
3012
|
async function credentialsRm(name, opts, deps) {
|
|
@@ -2922,9 +3123,9 @@ function formatWebhooksHuman(webhooks2) {
|
|
|
2922
3123
|
]);
|
|
2923
3124
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2924
3125
|
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(
|
|
3126
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
3127
|
+
const lines = [chalk19.bold(join4(padCells(HEADERS3)))];
|
|
3128
|
+
for (const row of rows) lines.push(join4(padCells(row)));
|
|
2928
3129
|
return lines.join("\n") + "\n";
|
|
2929
3130
|
}
|
|
2930
3131
|
async function webhooksList(opts, deps) {
|
|
@@ -3199,6 +3400,72 @@ Suggested line for your project's ${chalk20.bold("CLAUDE.md")} (or ${chalk20.bol
|
|
|
3199
3400
|
);
|
|
3200
3401
|
}
|
|
3201
3402
|
|
|
3403
|
+
// src/lib/update-check.ts
|
|
3404
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
3405
|
+
import { homedir as homedir2 } from "os";
|
|
3406
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
3407
|
+
var CACHE_FILE = join3(homedir2(), ".vibeview", "update-check.json");
|
|
3408
|
+
var REGISTRY_URL = "https://registry.npmjs.org/vibeview/latest";
|
|
3409
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3410
|
+
function isNewerVersion(a, b) {
|
|
3411
|
+
const parse = (v) => /^\d+\.\d+\.\d+$/.test(v) ? v.split(".").map(Number) : null;
|
|
3412
|
+
const x = parse(a);
|
|
3413
|
+
const y = parse(b);
|
|
3414
|
+
if (!x || !y) return false;
|
|
3415
|
+
for (let i = 0; i < 3; i++) {
|
|
3416
|
+
if (x[i] !== y[i]) return x[i] > y[i];
|
|
3417
|
+
}
|
|
3418
|
+
return false;
|
|
3419
|
+
}
|
|
3420
|
+
function updateCheckAllowed(argv, env2, stderrIsTTY) {
|
|
3421
|
+
if (!stderrIsTTY) return false;
|
|
3422
|
+
if (env2.CI || env2.NO_UPDATE_NOTIFIER || env2.VIBEVIEW_NO_UPDATE_CHECK) return false;
|
|
3423
|
+
const args = argv.slice(2);
|
|
3424
|
+
if (args[0] === "mcp") return false;
|
|
3425
|
+
if (args.includes("--json")) return false;
|
|
3426
|
+
const output = args.findIndex((a) => a === "--output" || a.startsWith("--output="));
|
|
3427
|
+
if (output !== -1) {
|
|
3428
|
+
const value = args[output].includes("=") ? args[output].split("=")[1] : args[output + 1];
|
|
3429
|
+
if (value && value !== "human") return false;
|
|
3430
|
+
}
|
|
3431
|
+
return true;
|
|
3432
|
+
}
|
|
3433
|
+
function updateNotice(current, latest) {
|
|
3434
|
+
return `A newer vibeview is available: ${current} \u2192 ${latest}. Run \`npm i -g vibeview@latest\`, or \`npx vibeview@latest ...\`.`;
|
|
3435
|
+
}
|
|
3436
|
+
function readCache(file) {
|
|
3437
|
+
try {
|
|
3438
|
+
const parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
3439
|
+
if (typeof parsed?.checked_at === "number" && typeof parsed?.latest === "string") {
|
|
3440
|
+
return parsed;
|
|
3441
|
+
}
|
|
3442
|
+
} catch {
|
|
3443
|
+
}
|
|
3444
|
+
return null;
|
|
3445
|
+
}
|
|
3446
|
+
function checkForUpdate(current, opts) {
|
|
3447
|
+
const file = opts.cacheFile ?? CACHE_FILE;
|
|
3448
|
+
const now = opts.now ?? Date.now();
|
|
3449
|
+
const cache = readCache(file);
|
|
3450
|
+
if (cache && isNewerVersion(cache.latest, current)) {
|
|
3451
|
+
opts.print(updateNotice(current, cache.latest));
|
|
3452
|
+
}
|
|
3453
|
+
if (cache && now - cache.checked_at < ONE_DAY_MS) return Promise.resolve();
|
|
3454
|
+
const fetchLatest = opts.fetchLatest ?? fetchLatestFromRegistry;
|
|
3455
|
+
return fetchLatest().then((latest) => {
|
|
3456
|
+
if (!latest) return;
|
|
3457
|
+
mkdirSync3(dirname2(file), { recursive: true });
|
|
3458
|
+
writeFileSync(file, JSON.stringify({ checked_at: now, latest }) + "\n");
|
|
3459
|
+
}).catch(() => {
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
async function fetchLatestFromRegistry() {
|
|
3463
|
+
const resp = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(1500) });
|
|
3464
|
+
if (!resp.ok) return null;
|
|
3465
|
+
const body = await resp.json();
|
|
3466
|
+
return typeof body.version === "string" ? body.version : null;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3202
3469
|
// src/cli.ts
|
|
3203
3470
|
var require2 = createRequire(import.meta.url);
|
|
3204
3471
|
var pkg = require2("../package.json");
|
|
@@ -3239,7 +3506,9 @@ function resolveMetadata(opts) {
|
|
|
3239
3506
|
}
|
|
3240
3507
|
}
|
|
3241
3508
|
var program = new Command();
|
|
3242
|
-
program.name("vibeview").description(
|
|
3509
|
+
program.name("vibeview").description(
|
|
3510
|
+
"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)"
|
|
3511
|
+
).version(pkg.version);
|
|
3243
3512
|
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
3513
|
"--device-category <category>",
|
|
3245
3514
|
"Device category: phone, tablet, tv (default: phone when --device is not set)",
|
|
@@ -3335,7 +3604,12 @@ program.command("list-devices").description("List devices registered with the pl
|
|
|
3335
3604
|
program.command("upload-app <file>").description(
|
|
3336
3605
|
"Upload an app build (zipped simulator .app for iOS/tvOS, .apk for Android/Android TV, or a Roku channel .zip)"
|
|
3337
3606
|
).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)").
|
|
3607
|
+
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(
|
|
3608
|
+
new Option(
|
|
3609
|
+
"--if-exists <mode>",
|
|
3610
|
+
"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"
|
|
3611
|
+
).choices(["fail", "update", "new"])
|
|
3612
|
+
).action((name, opts) => createTest({ ...opts, testName: name || opts.testName || "CLI Test" }));
|
|
3339
3613
|
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
3614
|
program.command("logout").description("Revoke the saved login token and remove it from ~/.vibeview/config.json").action(() => logout());
|
|
3341
3615
|
program.command("dev").description(
|
|
@@ -3501,7 +3775,7 @@ program.command("dev-reload").description(
|
|
|
3501
3775
|
program.command("mcp").description(
|
|
3502
3776
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
|
|
3503
3777
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
3504
|
-
const { runMcpServer } = await import("./server-
|
|
3778
|
+
const { runMcpServer } = await import("./server-6SRFOQ6B.js");
|
|
3505
3779
|
await runMcpServer({ ...opts, version: pkg.version });
|
|
3506
3780
|
});
|
|
3507
3781
|
program.command("agent-setup").description(
|
|
@@ -3525,6 +3799,10 @@ ${pressButtonsHelp()}`);
|
|
|
3525
3799
|
return runAgentVerb(cmd, positionals, opts);
|
|
3526
3800
|
});
|
|
3527
3801
|
}
|
|
3802
|
+
if (updateCheckAllowed(process.argv, process.env, process.stderr.isTTY)) {
|
|
3803
|
+
void checkForUpdate(pkg.version, { print: (line) => process.stderr.write(`${line}
|
|
3804
|
+
`) });
|
|
3805
|
+
}
|
|
3528
3806
|
program.parseAsync().catch((e) => {
|
|
3529
3807
|
if (e instanceof Error && e.name === "ExitPromptError") {
|
|
3530
3808
|
process.exit(130);
|