vibeview 0.27.0 → 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-U7FCGMWG.js → chunk-2JLTC67A.js} +159 -11
- package/dist/{chunk-46X7UN6Y.js → chunk-OKOAWGWV.js} +226 -161
- package/dist/cli.js +409 -95
- package/dist/{server-XJREB7KU.js → server-6SRFOQ6B.js} +298 -53
- package/dist/{upload-app-QUUXKNR2.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
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
formatDeviceModelsHuman,
|
|
12
12
|
formatDevicesHuman,
|
|
13
13
|
formatDuration,
|
|
14
|
-
formatUtcTimestamp,
|
|
15
14
|
killOwnedMetro,
|
|
16
15
|
listBuilds,
|
|
17
16
|
loadDevConfig,
|
|
@@ -23,22 +22,27 @@ import {
|
|
|
23
22
|
readDevState,
|
|
24
23
|
readMetroState,
|
|
25
24
|
runAgentVerb
|
|
26
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-OKOAWGWV.js";
|
|
27
26
|
import {
|
|
28
27
|
ApiClient,
|
|
28
|
+
ApiStatusError,
|
|
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,
|
|
35
37
|
errorExitCode,
|
|
38
|
+
formatUtcTimestamp,
|
|
36
39
|
loadConfig,
|
|
40
|
+
printJsonError,
|
|
37
41
|
reportCommandError,
|
|
38
42
|
resolveAuth,
|
|
39
43
|
saveConfig,
|
|
40
44
|
uploadApp
|
|
41
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-2JLTC67A.js";
|
|
42
46
|
|
|
43
47
|
// src/cli.ts
|
|
44
48
|
import { createRequire } from "module";
|
|
@@ -52,6 +56,26 @@ import chalk2 from "chalk";
|
|
|
52
56
|
|
|
53
57
|
// src/lib/formatters/human.ts
|
|
54
58
|
import chalk from "chalk";
|
|
59
|
+
|
|
60
|
+
// src/lib/formatters/cases.ts
|
|
61
|
+
function resultCases(result) {
|
|
62
|
+
if (Array.isArray(result.test_runs)) return result.test_runs;
|
|
63
|
+
const failedStep = result.steps?.find((s) => s.status !== "passed");
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
case_name: result.case_name || result.run_id,
|
|
67
|
+
status: result.status,
|
|
68
|
+
duration_ms: result.duration_ms,
|
|
69
|
+
error_message: result.error_message ?? failedStep?.error ?? null,
|
|
70
|
+
error_kind: failedStep?.error_kind ?? null,
|
|
71
|
+
error_kind_description: failedStep?.error_kind_description ?? null,
|
|
72
|
+
screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
|
|
73
|
+
url: result.url
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/lib/formatters/human.ts
|
|
55
79
|
function failureLines(failure) {
|
|
56
80
|
const lines = [];
|
|
57
81
|
if (failure.error_kind) {
|
|
@@ -72,20 +96,19 @@ function formatHuman(result, verbose) {
|
|
|
72
96
|
const icon = result.status === "passed" ? chalk.green("PASS") : chalk.red("FAIL");
|
|
73
97
|
const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
74
98
|
lines.push("");
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const passed = result.test_runs.filter((t) => t.status === "passed").length;
|
|
81
|
-
const total = result.test_runs.length;
|
|
99
|
+
const title = result.suite_name || result.case_name || result.run_id;
|
|
100
|
+
lines.push(` ${icon} ${chalk.bold(title)} ${chalk.dim(duration)}`);
|
|
101
|
+
const cases = resultCases(result);
|
|
102
|
+
const passed = cases.filter((t) => t.status === "passed").length;
|
|
103
|
+
const total = cases.length;
|
|
82
104
|
lines.push(` ${chalk.dim(`${passed}/${total} cases passed`)}`);
|
|
83
105
|
if (result.url) {
|
|
84
106
|
lines.push(` ${chalk.dim(result.url)}`);
|
|
85
107
|
}
|
|
86
|
-
|
|
108
|
+
const suiteCases = result.test_runs ?? [];
|
|
109
|
+
if (suiteCases.length > 0) {
|
|
87
110
|
lines.push("");
|
|
88
|
-
for (const tr of
|
|
111
|
+
for (const tr of suiteCases) {
|
|
89
112
|
const caseIcon = tr.status === "passed" ? chalk.green(" PASS") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
|
|
90
113
|
const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
91
114
|
lines.push(` ${caseIcon} ${tr.case_name} ${chalk.dim(caseDuration)}`);
|
|
@@ -99,7 +122,7 @@ function formatHuman(result, verbose) {
|
|
|
99
122
|
}
|
|
100
123
|
if (result.steps && result.steps.length > 0) {
|
|
101
124
|
lines.push("");
|
|
102
|
-
if (result.case_name) {
|
|
125
|
+
if (result.case_name && result.case_name !== title) {
|
|
103
126
|
lines.push(` ${chalk.bold(result.case_name)}`);
|
|
104
127
|
}
|
|
105
128
|
for (const step of result.steps) {
|
|
@@ -134,10 +157,11 @@ function escapeXml(s) {
|
|
|
134
157
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
135
158
|
}
|
|
136
159
|
function formatJunit(result) {
|
|
137
|
-
const suiteName = escapeXml(result.suite_name || result.run_id);
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
160
|
+
const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
|
|
161
|
+
const cases = resultCases(result);
|
|
162
|
+
const tests = cases.length;
|
|
163
|
+
const failures = cases.filter((t) => t.status === "failed").length;
|
|
164
|
+
const errors = cases.filter((t) => t.status === "error").length;
|
|
141
165
|
const totalTime = result.duration_ms ? (result.duration_ms / 1e3).toFixed(1) : "0";
|
|
142
166
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
143
167
|
const lines = [];
|
|
@@ -148,31 +172,64 @@ function formatJunit(result) {
|
|
|
148
172
|
lines.push(
|
|
149
173
|
` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}" timestamp="${timestamp}">`
|
|
150
174
|
);
|
|
151
|
-
|
|
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
|
+
}
|
|
180
|
+
for (const tr of cases) {
|
|
152
181
|
const caseName = escapeXml(tr.case_name);
|
|
153
182
|
const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
|
|
154
|
-
|
|
183
|
+
const out = tr.url ? ` <system-out>${escapeXml(tr.url)}</system-out>` : null;
|
|
184
|
+
if (tr.status === "passed" && !out) {
|
|
155
185
|
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
|
|
156
|
-
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
189
|
+
if (tr.status === "failed") {
|
|
157
190
|
const msg = escapeXml(tr.error_message || "Test failed");
|
|
158
|
-
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
159
191
|
lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
|
|
160
|
-
|
|
161
|
-
} else {
|
|
192
|
+
} else if (tr.status !== "passed") {
|
|
162
193
|
const msg = escapeXml(tr.error_message || "Infrastructure error");
|
|
163
|
-
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
164
194
|
lines.push(` <error message="${msg}" type="InfraError">${msg}</error>`);
|
|
165
|
-
lines.push(" </testcase>");
|
|
166
195
|
}
|
|
196
|
+
if (out) lines.push(out);
|
|
197
|
+
lines.push(" </testcase>");
|
|
167
198
|
}
|
|
168
199
|
lines.push(" </testsuite>");
|
|
169
200
|
lines.push("</testsuites>");
|
|
170
201
|
return lines.join("\n");
|
|
171
202
|
}
|
|
172
|
-
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);
|
|
173
229
|
const tests = result.suite_runs.reduce((sum, sr) => sum + sr.passed + sr.failed, 0);
|
|
174
230
|
const failures = result.suite_runs.reduce((sum, sr) => sum + sr.failed, 0);
|
|
175
|
-
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);
|
|
176
233
|
const timestamp = result.started_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
177
234
|
const lines = [];
|
|
178
235
|
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
@@ -183,43 +240,113 @@ function formatMultiSuiteJunit(result) {
|
|
|
183
240
|
const suiteName = escapeXml(sr.suite_name || sr.suite_id);
|
|
184
241
|
const suiteTests = sr.passed + sr.failed;
|
|
185
242
|
const suiteFails = sr.failed;
|
|
186
|
-
const suiteErrors = sr.status
|
|
243
|
+
const suiteErrors = errored(sr.status) ? 1 : 0;
|
|
187
244
|
lines.push(
|
|
188
245
|
` <testsuite name="${suiteName}" tests="${suiteTests}" failures="${suiteFails}" errors="${suiteErrors}" timestamp="${timestamp}">`
|
|
189
246
|
);
|
|
190
|
-
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) {
|
|
191
256
|
lines.push(` <error message="Suite errored" type="InfraError" />`);
|
|
192
257
|
}
|
|
193
258
|
lines.push(" </testsuite>");
|
|
194
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
|
+
}
|
|
195
268
|
lines.push("</testsuites>");
|
|
196
269
|
return lines.join("\n");
|
|
197
270
|
}
|
|
198
271
|
|
|
199
272
|
// src/lib/poll.ts
|
|
200
|
-
function
|
|
201
|
-
|
|
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;
|
|
202
282
|
spinner.stop();
|
|
203
283
|
process.stderr.write(line + "\n");
|
|
204
|
-
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
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function keepAliveEnabled(output) {
|
|
329
|
+
return output !== "json" && output !== "junit";
|
|
205
330
|
}
|
|
206
|
-
async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObserved) {
|
|
331
|
+
async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObserved, opts = {}) {
|
|
207
332
|
const deadline = Date.now() + timeoutMs;
|
|
208
333
|
const isCI = process.env.CI === "true" || process.env.CI === "1" || !process.stdout.isTTY;
|
|
334
|
+
const keepAlive = isCI && opts.keepAlive !== false;
|
|
209
335
|
let lastLogTime = Date.now();
|
|
210
336
|
let lastSeenEventCount = 0;
|
|
211
337
|
let totalSteps;
|
|
212
338
|
let currentStepLabel = "";
|
|
339
|
+
const printedSteps = /* @__PURE__ */ new Set();
|
|
213
340
|
let cachedDeviceLabel = null;
|
|
214
341
|
const caseStateBefore = /* @__PURE__ */ new Map();
|
|
215
342
|
const caseStartedPrinted = /* @__PURE__ */ new Set();
|
|
216
343
|
while (Date.now() < deadline) {
|
|
217
|
-
const result = await client.getRunStatus(runId);
|
|
344
|
+
const result = await retryTransient(() => client.getRunStatus(runId), { deadline });
|
|
218
345
|
const progress = result.progress || [];
|
|
219
346
|
if (progress.length > lastSeenEventCount) {
|
|
220
347
|
const newEvents = progress.slice(lastSeenEventCount);
|
|
221
348
|
for (const evt of newEvents) {
|
|
222
|
-
const ctx = { spinner, totalSteps, currentStepLabel };
|
|
349
|
+
const ctx = { spinner, totalSteps, currentStepLabel, printedSteps };
|
|
223
350
|
displayProgressEvent(evt, ctx);
|
|
224
351
|
totalSteps = ctx.totalSteps;
|
|
225
352
|
currentStepLabel = ctx.currentStepLabel;
|
|
@@ -252,6 +379,20 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
252
379
|
}
|
|
253
380
|
}
|
|
254
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
|
+
}
|
|
255
396
|
return result;
|
|
256
397
|
}
|
|
257
398
|
if (result.status === "queued" && result.queue) {
|
|
@@ -292,20 +433,24 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
292
433
|
spinner.text = `Status: ${result.status}...`;
|
|
293
434
|
}
|
|
294
435
|
}
|
|
295
|
-
if (
|
|
296
|
-
|
|
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
|
+
`);
|
|
297
440
|
lastLogTime = Date.now();
|
|
298
441
|
}
|
|
299
442
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
300
443
|
}
|
|
301
|
-
throw new
|
|
444
|
+
throw new PollTimeoutError(
|
|
445
|
+
`Timeout: run did not complete within ${Math.round(timeoutMs / 1e3)}s`
|
|
446
|
+
);
|
|
302
447
|
}
|
|
303
448
|
function formatAction(action) {
|
|
304
449
|
const tool = action.tool;
|
|
305
450
|
const args = action.args || {};
|
|
306
451
|
switch (tool) {
|
|
307
452
|
case "tap":
|
|
308
|
-
return `tap ${args.element_ref
|
|
453
|
+
return `tap ${args.ref ?? args.element_ref ?? ""}`.trimEnd();
|
|
309
454
|
case "tap_coordinates":
|
|
310
455
|
return `tap (${args.x}, ${args.y})`;
|
|
311
456
|
case "type_text":
|
|
@@ -333,7 +478,8 @@ function displayProgressEvent(evt, ctx) {
|
|
|
333
478
|
case "run_started":
|
|
334
479
|
printBelowSpinner(
|
|
335
480
|
spinner,
|
|
336
|
-
`${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..."
|
|
337
483
|
);
|
|
338
484
|
ctx.totalSteps = evt.total_steps ?? ctx.totalSteps;
|
|
339
485
|
break;
|
|
@@ -345,23 +491,17 @@ function displayProgressEvent(evt, ctx) {
|
|
|
345
491
|
break;
|
|
346
492
|
}
|
|
347
493
|
case "step_completed": {
|
|
348
|
-
const
|
|
494
|
+
const stepIndex = evt.step_index ?? 0;
|
|
495
|
+
const stepNum = stepIndex + 1;
|
|
349
496
|
const total = ctx.totalSteps ?? evt.total_steps ?? "?";
|
|
350
|
-
|
|
497
|
+
if (ctx.printedSteps.has(stepIndex)) break;
|
|
498
|
+
ctx.printedSteps.add(stepIndex);
|
|
351
499
|
spinner.stop();
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (evt.error) {
|
|
361
|
-
process.stderr.write(` ${chalk2.red(evt.error)}
|
|
362
|
-
`);
|
|
363
|
-
}
|
|
364
|
-
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
|
+
);
|
|
365
505
|
break;
|
|
366
506
|
}
|
|
367
507
|
case "conversation_turn": {
|
|
@@ -380,7 +520,7 @@ function displayProgressEvent(evt, ctx) {
|
|
|
380
520
|
`${icon} Run ${evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
|
|
381
521
|
`
|
|
382
522
|
);
|
|
383
|
-
spinner
|
|
523
|
+
resumeSpinner(spinner);
|
|
384
524
|
break;
|
|
385
525
|
}
|
|
386
526
|
case "app_installing":
|
|
@@ -393,6 +533,17 @@ function displayProgressEvent(evt, ctx) {
|
|
|
393
533
|
break;
|
|
394
534
|
}
|
|
395
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
|
+
}
|
|
396
547
|
function outputResult(result, options) {
|
|
397
548
|
switch (options.output) {
|
|
398
549
|
case "json":
|
|
@@ -418,6 +569,7 @@ async function runSuite(suiteId, options) {
|
|
|
418
569
|
const spinner = createSpinner("Starting suite run...");
|
|
419
570
|
let sessionId;
|
|
420
571
|
let lastQueueEntryId;
|
|
572
|
+
let runId;
|
|
421
573
|
let cleaning = false;
|
|
422
574
|
const cleanup = async (signal) => {
|
|
423
575
|
if (cleaning) process.exit(130);
|
|
@@ -478,6 +630,7 @@ ${signal} received.
|
|
|
478
630
|
metadata: options.metadata
|
|
479
631
|
});
|
|
480
632
|
sessionId = response.session_id;
|
|
633
|
+
runId = response.run_id;
|
|
481
634
|
spinner.stop();
|
|
482
635
|
process.stderr.write(
|
|
483
636
|
`${chalk3.dim("Suite:")} ${suiteId}
|
|
@@ -494,7 +647,8 @@ ${chalk3.dim("Run:")} ${response.run_id}
|
|
|
494
647
|
spinner,
|
|
495
648
|
(entryId) => {
|
|
496
649
|
lastQueueEntryId = entryId;
|
|
497
|
-
}
|
|
650
|
+
},
|
|
651
|
+
{ keepAlive: keepAliveEnabled(options.output) }
|
|
498
652
|
);
|
|
499
653
|
spinner.stop();
|
|
500
654
|
if (process.stdin.isTTY) {
|
|
@@ -505,6 +659,11 @@ ${chalk3.dim("Run:")} ${response.run_id}
|
|
|
505
659
|
process.exit(mapExitCode(result.status));
|
|
506
660
|
} catch (err) {
|
|
507
661
|
spinner.fail(err instanceof Error ? err.message : "Unknown error");
|
|
662
|
+
outputRunError(err, options, {
|
|
663
|
+
name: suiteId,
|
|
664
|
+
runId,
|
|
665
|
+
url: runId ? `${baseUrl}/tests/runs/${runId}` : void 0
|
|
666
|
+
});
|
|
508
667
|
if (lastQueueEntryId !== void 0) {
|
|
509
668
|
try {
|
|
510
669
|
await client.cancelQueueEntry(lastQueueEntryId);
|
|
@@ -593,10 +752,15 @@ ${signal} received.
|
|
|
593
752
|
const msg = err instanceof Error ? err.message : String(err);
|
|
594
753
|
if (msg.startsWith("Not found:") || msg.includes("404") || /No suites match tags/.test(msg)) {
|
|
595
754
|
const suffix = opts.platform ? ` on platform ${opts.platform}` : "";
|
|
596
|
-
|
|
755
|
+
const noMatch = `No suites match tags: ${opts.tag.join(", ")}${suffix}`;
|
|
756
|
+
if (opts.output === "json") printJsonError(noMatch);
|
|
757
|
+
if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", noMatch));
|
|
758
|
+
process.stderr.write(chalk4.yellow(`${noMatch}
|
|
597
759
|
`));
|
|
598
760
|
process.exit(EXIT_INFRA_ERROR);
|
|
599
761
|
}
|
|
762
|
+
if (opts.output === "json") printJsonError(err);
|
|
763
|
+
if (opts.output === "junit") console.log(formatJunitError("vibeview run-suites", msg));
|
|
600
764
|
process.stderr.write(chalk4.red(`Failed to start: ${msg}
|
|
601
765
|
`));
|
|
602
766
|
process.exit(EXIT_INFRA_ERROR);
|
|
@@ -635,8 +799,47 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
635
799
|
};
|
|
636
800
|
const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
637
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
|
+
};
|
|
638
834
|
while (Date.now() < deadline) {
|
|
639
|
-
|
|
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
|
+
}
|
|
640
843
|
let doneCount = 0;
|
|
641
844
|
let queuedChild;
|
|
642
845
|
for (const sr of last.suite_runs) {
|
|
@@ -671,18 +874,19 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
671
874
|
} else {
|
|
672
875
|
spinner.text = `Running suites \u2014 ${doneCount}/${totalMatched} complete...`;
|
|
673
876
|
}
|
|
674
|
-
if (
|
|
877
|
+
if (TERMINAL.has(last.status)) break;
|
|
675
878
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
676
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
|
+
}
|
|
677
885
|
spinner.stop();
|
|
678
886
|
if (process.stdin.isTTY) {
|
|
679
887
|
process.stdin.setRawMode(false);
|
|
680
888
|
process.stdin.pause();
|
|
681
889
|
}
|
|
682
|
-
if (!last) {
|
|
683
|
-
process.stderr.write(chalk4.red("Timed out before any status was returned.\n"));
|
|
684
|
-
process.exit(EXIT_INFRA_ERROR);
|
|
685
|
-
}
|
|
686
890
|
if (opts.output === "json") {
|
|
687
891
|
process.stdout.write(JSON.stringify(last, null, 2) + "\n");
|
|
688
892
|
} else if (opts.output === "junit") {
|
|
@@ -728,6 +932,7 @@ async function runTest(testId, options) {
|
|
|
728
932
|
const client = new ApiClient(baseUrl, token);
|
|
729
933
|
let sessionId;
|
|
730
934
|
let lastQueueEntryId;
|
|
935
|
+
let runId;
|
|
731
936
|
const spinner = createSpinner("Starting test run...");
|
|
732
937
|
let cleaning = false;
|
|
733
938
|
const cleanup = async (signal) => {
|
|
@@ -789,6 +994,7 @@ ${signal} received.
|
|
|
789
994
|
metadata: options.metadata
|
|
790
995
|
});
|
|
791
996
|
sessionId = response.session_id;
|
|
997
|
+
runId = response.run_id;
|
|
792
998
|
spinner.stop();
|
|
793
999
|
process.stderr.write(
|
|
794
1000
|
`${chalk5.dim("Session:")} ${sessionId || "unknown"}
|
|
@@ -806,7 +1012,8 @@ ${chalk5.dim("Run:")} ${response.run_id}
|
|
|
806
1012
|
spinner,
|
|
807
1013
|
(entryId) => {
|
|
808
1014
|
lastQueueEntryId = entryId;
|
|
809
|
-
}
|
|
1015
|
+
},
|
|
1016
|
+
{ keepAlive: keepAliveEnabled(options.output) }
|
|
810
1017
|
);
|
|
811
1018
|
spinner.stop();
|
|
812
1019
|
if (process.stdin.isTTY) {
|
|
@@ -817,6 +1024,11 @@ ${chalk5.dim("Run:")} ${response.run_id}
|
|
|
817
1024
|
process.exit(mapExitCode(result.status));
|
|
818
1025
|
} catch (err) {
|
|
819
1026
|
spinner.fail(err instanceof Error ? err.message : "Unknown error");
|
|
1027
|
+
outputRunError(err, options, {
|
|
1028
|
+
name: testId,
|
|
1029
|
+
runId,
|
|
1030
|
+
url: runId ? `${baseUrl}/tests/runs/${runId}` : void 0
|
|
1031
|
+
});
|
|
820
1032
|
if (lastQueueEntryId !== void 0) {
|
|
821
1033
|
try {
|
|
822
1034
|
await client.cancelQueueEntry(lastQueueEntryId);
|
|
@@ -862,7 +1074,9 @@ async function listSuites(options) {
|
|
|
862
1074
|
for (const s of suites) {
|
|
863
1075
|
const platform = s.platform ? chalk6.dim(` [${s.platform}]`) : "";
|
|
864
1076
|
const tagsText = s.tags && s.tags.length > 0 ? chalk6.dim(` [${s.tags.map((t) => t.display_name).join(", ")}]`) : "";
|
|
865
|
-
console.log(
|
|
1077
|
+
console.log(
|
|
1078
|
+
` ${chalk6.cyan(s.id)} ${s.name}${platform}${tagsText} (${s.case_count} ${s.case_count === 1 ? "case" : "cases"})`
|
|
1079
|
+
);
|
|
866
1080
|
}
|
|
867
1081
|
console.log();
|
|
868
1082
|
}
|
|
@@ -1017,9 +1231,12 @@ async function createTest(options) {
|
|
|
1017
1231
|
app_id: appPublicId || "",
|
|
1018
1232
|
steps,
|
|
1019
1233
|
context: options.context,
|
|
1020
|
-
mode: "ai"
|
|
1234
|
+
mode: "ai",
|
|
1235
|
+
if_exists: options.ifExists
|
|
1021
1236
|
});
|
|
1022
|
-
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
|
+
);
|
|
1023
1240
|
if (options.output === "json") {
|
|
1024
1241
|
console.log(
|
|
1025
1242
|
JSON.stringify(
|
|
@@ -1027,6 +1244,7 @@ async function createTest(options) {
|
|
|
1027
1244
|
test_id: result.public_id,
|
|
1028
1245
|
name: result.name,
|
|
1029
1246
|
app_id: appPublicId || null,
|
|
1247
|
+
outcome: result.outcome ?? null,
|
|
1030
1248
|
url: `${baseUrl}/tests?tab=standalone`
|
|
1031
1249
|
},
|
|
1032
1250
|
null,
|
|
@@ -1047,8 +1265,11 @@ async function createTest(options) {
|
|
|
1047
1265
|
`));
|
|
1048
1266
|
}
|
|
1049
1267
|
} catch (err) {
|
|
1268
|
+
if (options.output === "json") printJsonError(err);
|
|
1050
1269
|
console.error(chalk9.red(err instanceof Error ? err.message : "Failed to create test"));
|
|
1051
|
-
process.exit(
|
|
1270
|
+
process.exit(
|
|
1271
|
+
err instanceof ApiStatusError && err.status === 409 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR
|
|
1272
|
+
);
|
|
1052
1273
|
}
|
|
1053
1274
|
}
|
|
1054
1275
|
|
|
@@ -1130,6 +1351,11 @@ async function logout() {
|
|
|
1130
1351
|
|
|
1131
1352
|
// src/commands/dev-status.ts
|
|
1132
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
|
+
}
|
|
1133
1359
|
async function devStatus(opts) {
|
|
1134
1360
|
const state = readDevState(process.cwd());
|
|
1135
1361
|
if (!state) {
|
|
@@ -1144,12 +1370,14 @@ async function devStatus(opts) {
|
|
|
1144
1370
|
const { token, baseUrl } = resolveAuth(opts);
|
|
1145
1371
|
const api = new ApiClient(baseUrl, token);
|
|
1146
1372
|
let sessionStatus;
|
|
1373
|
+
let info;
|
|
1147
1374
|
try {
|
|
1148
|
-
|
|
1375
|
+
info = await api.getSession(state.session_id);
|
|
1149
1376
|
sessionStatus = info.status;
|
|
1150
1377
|
} catch {
|
|
1151
1378
|
sessionStatus = void 0;
|
|
1152
1379
|
}
|
|
1380
|
+
const build = buildLine(info);
|
|
1153
1381
|
const metro = readMetroState(process.cwd());
|
|
1154
1382
|
if (opts.json) {
|
|
1155
1383
|
console.log(
|
|
@@ -1163,6 +1391,9 @@ async function devStatus(opts) {
|
|
|
1163
1391
|
metro_port: state.metro_port,
|
|
1164
1392
|
started_at: state.started_at,
|
|
1165
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,
|
|
1166
1397
|
...metro ? { owned_metro_pid: metro.pid, owned_metro_port: metro.port } : {}
|
|
1167
1398
|
})
|
|
1168
1399
|
);
|
|
@@ -1171,6 +1402,7 @@ async function devStatus(opts) {
|
|
|
1171
1402
|
console.error(` session: ${state.session_id}${sessionStatus ? ` (${sessionStatus})` : ""}`);
|
|
1172
1403
|
console.error(` platform: ${state.platform}`);
|
|
1173
1404
|
console.error(` app: ${state.app_id}`);
|
|
1405
|
+
if (build) console.error(` build: ${build}`);
|
|
1174
1406
|
console.error(` url: ${state.page_url}`);
|
|
1175
1407
|
console.error(` started: ${state.started_at}`);
|
|
1176
1408
|
}
|
|
@@ -1322,13 +1554,13 @@ function formatBuildsHuman(builds2) {
|
|
|
1322
1554
|
]);
|
|
1323
1555
|
const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1324
1556
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
1325
|
-
const
|
|
1326
|
-
const lines = [chalk12.bold(
|
|
1557
|
+
const join4 = (cells) => cells.join(" ").trimEnd();
|
|
1558
|
+
const lines = [chalk12.bold(join4(padCells(HEADERS)))];
|
|
1327
1559
|
for (const row of rows) {
|
|
1328
1560
|
const padded = padCells(row);
|
|
1329
1561
|
const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
|
|
1330
1562
|
padded[2] = colorFn(row[2]) + padded[2].slice(row[2].length);
|
|
1331
|
-
lines.push(
|
|
1563
|
+
lines.push(join4(padded));
|
|
1332
1564
|
}
|
|
1333
1565
|
return lines.join("\n") + "\n";
|
|
1334
1566
|
}
|
|
@@ -1915,9 +2147,9 @@ function formatVersionsHuman(versions2) {
|
|
|
1915
2147
|
const rows = versions2.map((v) => [v.platform, String(v.build_number), v.updated_at ?? ""]);
|
|
1916
2148
|
const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1917
2149
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
1918
|
-
const
|
|
1919
|
-
const lines = [chalk15.bold(
|
|
1920
|
-
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)));
|
|
1921
2153
|
return lines.join("\n") + "\n";
|
|
1922
2154
|
}
|
|
1923
2155
|
function printError3(opts, err) {
|
|
@@ -2074,9 +2306,9 @@ function formatEnvVarsHuman(variables) {
|
|
|
2074
2306
|
]);
|
|
2075
2307
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2076
2308
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2077
|
-
const
|
|
2078
|
-
const lines = [chalk17.bold(
|
|
2079
|
-
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)));
|
|
2080
2312
|
return lines.join("\n") + "\n";
|
|
2081
2313
|
}
|
|
2082
2314
|
async function envSet(name, value, opts, deps) {
|
|
@@ -2616,9 +2848,9 @@ function formatAscKeysHuman(keys) {
|
|
|
2616
2848
|
const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, formatUtcTimestamp(k.created_at)]);
|
|
2617
2849
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2618
2850
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2619
|
-
const
|
|
2620
|
-
const lines = [chalk18.bold(
|
|
2621
|
-
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)));
|
|
2622
2854
|
return lines.join("\n") + "\n";
|
|
2623
2855
|
}
|
|
2624
2856
|
async function ascKeyRm(name, opts, deps) {
|
|
@@ -2692,9 +2924,9 @@ function formatPlayKeysHuman(keys) {
|
|
|
2692
2924
|
const rows = keys.map((k) => [k.name, k.client_email, formatUtcTimestamp(k.created_at)]);
|
|
2693
2925
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2694
2926
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2695
|
-
const
|
|
2696
|
-
const lines = [chalk18.bold(
|
|
2697
|
-
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)));
|
|
2698
2930
|
return lines.join("\n") + "\n";
|
|
2699
2931
|
}
|
|
2700
2932
|
async function playKeyRm(name, opts, deps) {
|
|
@@ -2772,9 +3004,9 @@ function formatCredentialsHuman(credentials2) {
|
|
|
2772
3004
|
]);
|
|
2773
3005
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2774
3006
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2775
|
-
const
|
|
2776
|
-
const lines = [chalk18.bold(
|
|
2777
|
-
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)));
|
|
2778
3010
|
return lines.join("\n") + "\n";
|
|
2779
3011
|
}
|
|
2780
3012
|
async function credentialsRm(name, opts, deps) {
|
|
@@ -2891,9 +3123,9 @@ function formatWebhooksHuman(webhooks2) {
|
|
|
2891
3123
|
]);
|
|
2892
3124
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2893
3125
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2894
|
-
const
|
|
2895
|
-
const lines = [chalk19.bold(
|
|
2896
|
-
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)));
|
|
2897
3129
|
return lines.join("\n") + "\n";
|
|
2898
3130
|
}
|
|
2899
3131
|
async function webhooksList(opts, deps) {
|
|
@@ -3168,6 +3400,72 @@ Suggested line for your project's ${chalk20.bold("CLAUDE.md")} (or ${chalk20.bol
|
|
|
3168
3400
|
);
|
|
3169
3401
|
}
|
|
3170
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
|
+
|
|
3171
3469
|
// src/cli.ts
|
|
3172
3470
|
var require2 = createRequire(import.meta.url);
|
|
3173
3471
|
var pkg = require2("../package.json");
|
|
@@ -3208,7 +3506,9 @@ function resolveMetadata(opts) {
|
|
|
3208
3506
|
}
|
|
3209
3507
|
}
|
|
3210
3508
|
var program = new Command();
|
|
3211
|
-
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);
|
|
3212
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(
|
|
3213
3513
|
"--device-category <category>",
|
|
3214
3514
|
"Device category: phone, tablet, tv (default: phone when --device is not set)",
|
|
@@ -3291,7 +3591,12 @@ program.command("list-devices").description("List devices registered with the pl
|
|
|
3291
3591
|
new Option("--type <type>", "Filter by device type").choices(["ios", "android", "roku"])
|
|
3292
3592
|
).addOption(
|
|
3293
3593
|
new Option("--category <category>", "Filter by category").choices(["phone", "tablet", "tv"])
|
|
3294
|
-
).addOption(
|
|
3594
|
+
).addOption(
|
|
3595
|
+
new Option(
|
|
3596
|
+
"--status <status>",
|
|
3597
|
+
"Filter by status (updating = being updated, takes no new session yet)"
|
|
3598
|
+
).choices([...DEVICE_STATUS_FILTERS])
|
|
3599
|
+
).option(
|
|
3295
3600
|
"--models",
|
|
3296
3601
|
"List the device models you can start instead (the names `dev --model` takes)",
|
|
3297
3602
|
false
|
|
@@ -3299,7 +3604,12 @@ program.command("list-devices").description("List devices registered with the pl
|
|
|
3299
3604
|
program.command("upload-app <file>").description(
|
|
3300
3605
|
"Upload an app build (zipped simulator .app for iOS/tvOS, .apk for Android/Android TV, or a Roku channel .zip)"
|
|
3301
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));
|
|
3302
|
-
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" }));
|
|
3303
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));
|
|
3304
3614
|
program.command("logout").description("Revoke the saved login token and remove it from ~/.vibeview/config.json").action(() => logout());
|
|
3305
3615
|
program.command("dev").description(
|
|
@@ -3465,7 +3775,7 @@ program.command("dev-reload").description(
|
|
|
3465
3775
|
program.command("mcp").description(
|
|
3466
3776
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
|
|
3467
3777
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
3468
|
-
const { runMcpServer } = await import("./server-
|
|
3778
|
+
const { runMcpServer } = await import("./server-6SRFOQ6B.js");
|
|
3469
3779
|
await runMcpServer({ ...opts, version: pkg.version });
|
|
3470
3780
|
});
|
|
3471
3781
|
program.command("agent-setup").description(
|
|
@@ -3489,6 +3799,10 @@ ${pressButtonsHelp()}`);
|
|
|
3489
3799
|
return runAgentVerb(cmd, positionals, opts);
|
|
3490
3800
|
});
|
|
3491
3801
|
}
|
|
3802
|
+
if (updateCheckAllowed(process.argv, process.env, process.stderr.isTTY)) {
|
|
3803
|
+
void checkForUpdate(pkg.version, { print: (line) => process.stderr.write(`${line}
|
|
3804
|
+
`) });
|
|
3805
|
+
}
|
|
3492
3806
|
program.parseAsync().catch((e) => {
|
|
3493
3807
|
if (e instanceof Error && e.name === "ExitPromptError") {
|
|
3494
3808
|
process.exit(130);
|