vibeview 0.37.1 → 0.39.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/README.md +6 -5
- package/dist/{chunk-NJ2IXB5H.js → chunk-EQN3X62U.js} +33 -3
- package/dist/{chunk-QRJAFBH3.js → chunk-MYXUGCR5.js} +41 -5
- package/dist/cli.js +472 -232
- package/dist/{server-DF3CDWL4.js → server-GEY4WZUH.js} +201 -4
- package/dist/{upload-app-BPTGBDQB.js → upload-app-UHJLFQKZ.js} +1 -1
- package/package.json +2 -1
- package/skills/vibeview-agent/SKILL.md +6 -2
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
listBuilds,
|
|
16
16
|
loadDevConfig,
|
|
17
17
|
newTransitionState,
|
|
18
|
+
notCheckedSteps,
|
|
19
|
+
notCheckedSuffix,
|
|
20
|
+
outcomeOf,
|
|
18
21
|
platformLabel,
|
|
19
22
|
platformWithArticle,
|
|
20
23
|
pressButtonsHelp,
|
|
@@ -24,8 +27,10 @@ import {
|
|
|
24
27
|
readDevStateFile,
|
|
25
28
|
readMetroState,
|
|
26
29
|
recordDevUse,
|
|
27
|
-
runAgentVerb
|
|
28
|
-
|
|
30
|
+
runAgentVerb,
|
|
31
|
+
runStepsNotChecked,
|
|
32
|
+
stepsNotCheckedLabel
|
|
33
|
+
} from "./chunk-EQN3X62U.js";
|
|
29
34
|
import {
|
|
30
35
|
ApiClient,
|
|
31
36
|
ApiStatusError,
|
|
@@ -46,7 +51,7 @@ import {
|
|
|
46
51
|
resolveAuth,
|
|
47
52
|
saveConfig,
|
|
48
53
|
uploadApp
|
|
49
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-MYXUGCR5.js";
|
|
50
55
|
|
|
51
56
|
// src/cli.ts
|
|
52
57
|
import { createRequire } from "module";
|
|
@@ -61,21 +66,6 @@ import chalk2 from "chalk";
|
|
|
61
66
|
// src/lib/formatters/human.ts
|
|
62
67
|
import chalk from "chalk";
|
|
63
68
|
|
|
64
|
-
// src/lib/formatters/not-checked.ts
|
|
65
|
-
function notCheckedSteps(steps) {
|
|
66
|
-
return (steps ?? []).filter((s) => s.status === "not_checked");
|
|
67
|
-
}
|
|
68
|
-
function runStepsNotChecked(result) {
|
|
69
|
-
if (typeof result.steps_not_checked === "number") return result.steps_not_checked;
|
|
70
|
-
return notCheckedSteps(result.steps).length;
|
|
71
|
-
}
|
|
72
|
-
function stepsNotCheckedLabel(n) {
|
|
73
|
-
return `${n} step${n === 1 ? "" : "s"} not checked`;
|
|
74
|
-
}
|
|
75
|
-
function notCheckedSuffix(status, n) {
|
|
76
|
-
return status === "passed" && n && n > 0 ? ` (${stepsNotCheckedLabel(n)})` : "";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
69
|
// src/lib/formatters/cases.ts
|
|
80
70
|
function resultCases(result) {
|
|
81
71
|
if (Array.isArray(result.test_runs)) return result.test_runs;
|
|
@@ -90,6 +80,9 @@ function resultCases(result) {
|
|
|
90
80
|
error_kind_description: failedStep?.error_kind_description ?? null,
|
|
91
81
|
screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
|
|
92
82
|
url: result.url,
|
|
83
|
+
quarantined: result.quarantined,
|
|
84
|
+
not_checked: result.not_checked,
|
|
85
|
+
not_run: result.not_run,
|
|
93
86
|
steps_not_checked: runStepsNotChecked(result),
|
|
94
87
|
not_checked_steps: notCheckedSteps(result.steps).map((s) => ({
|
|
95
88
|
step: s.step,
|
|
@@ -117,19 +110,21 @@ function failureLines(failure) {
|
|
|
117
110
|
}
|
|
118
111
|
function formatHuman(result, verbose) {
|
|
119
112
|
const lines = [];
|
|
120
|
-
const
|
|
113
|
+
const outcome = outcomeOf(result);
|
|
114
|
+
const icon = outcome === "not_checked" ? chalk.gray("NOT CHECKED") : result.status === "passed" ? chalk.green("PASS") : result.status === "failed" && result.quarantined === true ? chalk.yellow("FAIL (quarantined)") : chalk.red("FAIL");
|
|
121
115
|
const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
122
116
|
lines.push("");
|
|
123
117
|
const title = result.suite_name || result.case_name || result.run_id;
|
|
124
118
|
const cases = resultCases(result);
|
|
125
119
|
const notChecked = cases.reduce((n, t) => n + (t.steps_not_checked ?? 0), 0);
|
|
126
|
-
const runSuffix = notCheckedSuffix(
|
|
120
|
+
const runSuffix = notCheckedSuffix(outcome, notChecked);
|
|
127
121
|
lines.push(
|
|
128
122
|
` ${icon}${runSuffix ? chalk.yellow(runSuffix) : ""} ${chalk.bold(title)} ${chalk.dim(duration)}`
|
|
129
123
|
);
|
|
130
|
-
const passed = cases.filter((t) => t
|
|
124
|
+
const passed = cases.filter((t) => outcomeOf(t) === "passed").length;
|
|
125
|
+
const notCheckedCases = cases.filter((t) => outcomeOf(t) === "not_checked").length;
|
|
131
126
|
const total = cases.length;
|
|
132
|
-
const notCheckedNote = notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
|
|
127
|
+
const notCheckedNote = notCheckedCases > 0 ? `, ${notCheckedCases} not checked` : notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
|
|
133
128
|
lines.push(` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}`)}`);
|
|
134
129
|
if (result.url) {
|
|
135
130
|
lines.push(` ${chalk.dim(result.url)}`);
|
|
@@ -138,16 +133,19 @@ function formatHuman(result, verbose) {
|
|
|
138
133
|
if (suiteCases.length > 0) {
|
|
139
134
|
lines.push("");
|
|
140
135
|
for (const tr of suiteCases) {
|
|
141
|
-
const
|
|
136
|
+
const caseOutcome = outcomeOf(tr);
|
|
137
|
+
const caseIcon = caseOutcome === "not_checked" ? chalk.gray(" NOT CHECKED") : caseOutcome === "not_run" ? chalk.gray(" NOT RUN") : tr.status === "passed" ? chalk.green(" PASS") : tr.status === "failed" && tr.quarantined === true ? chalk.yellow(" FAIL (quarantined)") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
|
|
142
138
|
const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
143
|
-
const caseSuffix = notCheckedSuffix(
|
|
139
|
+
const caseSuffix = notCheckedSuffix(caseOutcome, tr.steps_not_checked);
|
|
144
140
|
lines.push(
|
|
145
141
|
` ${caseIcon}${caseSuffix ? chalk.yellow(caseSuffix) : ""} ${tr.case_name} ${chalk.dim(caseDuration)}`
|
|
146
142
|
);
|
|
147
143
|
if (tr.url) {
|
|
148
144
|
lines.push(` ${chalk.dim(tr.url)}`);
|
|
149
145
|
}
|
|
150
|
-
if (
|
|
146
|
+
if (caseOutcome === "not_checked" || caseOutcome === "not_run") {
|
|
147
|
+
if (tr.error_message) lines.push(` ${chalk.dim(tr.error_message)}`);
|
|
148
|
+
} else if (tr.status !== "passed") {
|
|
151
149
|
lines.push(...failureLines(tr));
|
|
152
150
|
}
|
|
153
151
|
}
|
|
@@ -173,7 +171,10 @@ function formatHuman(result, verbose) {
|
|
|
173
171
|
}
|
|
174
172
|
}
|
|
175
173
|
}
|
|
176
|
-
if (result.error_message) {
|
|
174
|
+
if (result.error_message && outcome === "not_checked") {
|
|
175
|
+
lines.push(`
|
|
176
|
+
${chalk.dim(result.error_message)}`);
|
|
177
|
+
} else if (result.error_message) {
|
|
177
178
|
lines.push(`
|
|
178
179
|
${chalk.red("Error:")} ${result.error_message}`);
|
|
179
180
|
}
|
|
@@ -214,21 +215,29 @@ function caseDetailLines(c) {
|
|
|
214
215
|
if (out.length) lines.push(` <system-out>${escapeXml(out.join("\n"))}</system-out>`);
|
|
215
216
|
return lines;
|
|
216
217
|
}
|
|
218
|
+
var isQuarantinedFailure = (t) => t.status === "failed" && t.quarantined === true;
|
|
219
|
+
var NEUTRAL = /* @__PURE__ */ new Set(["not_checked", "not_run"]);
|
|
220
|
+
function neutralMessage(outcome, message) {
|
|
221
|
+
if (outcome === "not_checked") return `not checked: ${message || "no step was checked"}`;
|
|
222
|
+
return message || "Not run";
|
|
223
|
+
}
|
|
217
224
|
function formatJunit(result) {
|
|
218
225
|
const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
|
|
219
226
|
const cases = resultCases(result);
|
|
220
227
|
const tests = cases.length;
|
|
221
|
-
const failures = cases.filter((t) => t.status === "failed").length;
|
|
228
|
+
const failures = cases.filter((t) => t.status === "failed" && !isQuarantinedFailure(t)).length;
|
|
222
229
|
const errors = cases.filter((t) => t.status === "error").length;
|
|
230
|
+
const skipped = cases.filter((t) => isQuarantinedFailure(t) || NEUTRAL.has(outcomeOf(t))).length;
|
|
231
|
+
const skippedAttr = skipped > 0 ? ` skipped="${skipped}"` : "";
|
|
223
232
|
const totalTime = result.duration_ms ? (result.duration_ms / 1e3).toFixed(1) : "0";
|
|
224
233
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
225
234
|
const lines = [];
|
|
226
235
|
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
227
236
|
lines.push(
|
|
228
|
-
`<testsuites name="vibeview" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}">`
|
|
237
|
+
`<testsuites name="vibeview" tests="${tests}" failures="${failures}" errors="${errors}"${skippedAttr} time="${totalTime}">`
|
|
229
238
|
);
|
|
230
239
|
lines.push(
|
|
231
|
-
` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}" time="${totalTime}" timestamp="${timestamp}">`
|
|
240
|
+
` <testsuite name="${suiteName}" tests="${tests}" failures="${failures}" errors="${errors}"${skippedAttr} time="${totalTime}" timestamp="${timestamp}">`
|
|
232
241
|
);
|
|
233
242
|
if (result.url || result.build_warning) {
|
|
234
243
|
lines.push(" <properties>");
|
|
@@ -245,13 +254,21 @@ function formatJunit(result) {
|
|
|
245
254
|
for (const tr of cases) {
|
|
246
255
|
const caseName = escapeXml(tr.case_name);
|
|
247
256
|
const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
|
|
248
|
-
const
|
|
249
|
-
|
|
257
|
+
const outcome = outcomeOf(tr);
|
|
258
|
+
const detail = caseDetailLines({ ...tr, status: outcome });
|
|
259
|
+
if (outcome === "passed" && detail.length === 0) {
|
|
250
260
|
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
|
|
251
261
|
continue;
|
|
252
262
|
}
|
|
253
263
|
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}">`);
|
|
254
|
-
if (tr
|
|
264
|
+
if (isQuarantinedFailure(tr)) {
|
|
265
|
+
const msg = escapeXml(tr.error_message || "Test failed");
|
|
266
|
+
lines.push(` <skipped message="quarantined: ${msg}" />`);
|
|
267
|
+
} else if (NEUTRAL.has(outcome)) {
|
|
268
|
+
lines.push(
|
|
269
|
+
` <skipped message="${escapeXml(neutralMessage(outcome, tr.error_message))}" />`
|
|
270
|
+
);
|
|
271
|
+
} else if (tr.status === "failed") {
|
|
255
272
|
const msg = escapeXml(tr.error_message || "Test failed");
|
|
256
273
|
lines.push(` <failure message="${msg}" type="TestFailure">${msg}</failure>`);
|
|
257
274
|
} else if (tr.status !== "passed") {
|
|
@@ -287,7 +304,7 @@ function formatJunitError(name, message, url) {
|
|
|
287
304
|
lines.push("</testsuites>");
|
|
288
305
|
return lines.join("\n");
|
|
289
306
|
}
|
|
290
|
-
var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
307
|
+
var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
|
|
291
308
|
function caseLines(c, classname) {
|
|
292
309
|
const name = escapeXml(c.name);
|
|
293
310
|
const head = ` <testcase name="${name}" classname="${classname}" time="${c.time}"`;
|
|
@@ -324,6 +341,21 @@ function formatMultiSuiteJunit(result, opts = {}) {
|
|
|
324
341
|
url: c.url || void 0,
|
|
325
342
|
steps_not_checked: c.steps_not_checked
|
|
326
343
|
};
|
|
344
|
+
const outcome = outcomeOf(c);
|
|
345
|
+
if (NEUTRAL.has(outcome)) {
|
|
346
|
+
return {
|
|
347
|
+
...base,
|
|
348
|
+
verdict: "skipped",
|
|
349
|
+
message: neutralMessage(outcome, c.error_message)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (c.status === "failed" && c.quarantined === true) {
|
|
353
|
+
return {
|
|
354
|
+
...base,
|
|
355
|
+
verdict: "skipped",
|
|
356
|
+
message: `quarantined: ${c.error_message || "Test failed"}`
|
|
357
|
+
};
|
|
358
|
+
}
|
|
327
359
|
switch (c.status) {
|
|
328
360
|
case "passed":
|
|
329
361
|
return { ...base, verdict: "passed" };
|
|
@@ -395,6 +427,7 @@ function formatMultiSuiteJunit(result, opts = {}) {
|
|
|
395
427
|
}
|
|
396
428
|
|
|
397
429
|
// src/lib/poll.ts
|
|
430
|
+
var TERMINAL_STATUSES = ["passed", "failed", "error", "cancelled", "not_checked"];
|
|
398
431
|
function resumeSpinner(spinner, text) {
|
|
399
432
|
if (spinner.isEnabled === false) {
|
|
400
433
|
if (text !== void 0) spinner.text = text;
|
|
@@ -492,20 +525,26 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
492
525
|
`${chalk2.cyan("\u25B6")} ${chalk2.bold(tr.case_name || "Case")} started`
|
|
493
526
|
);
|
|
494
527
|
}
|
|
495
|
-
const isTerminal =
|
|
496
|
-
const wasTerminal = prev &&
|
|
528
|
+
const isTerminal = TERMINAL_STATUSES.includes(cur);
|
|
529
|
+
const wasTerminal = prev && TERMINAL_STATUSES.includes(prev);
|
|
497
530
|
if (isTerminal && !wasTerminal) {
|
|
498
|
-
const
|
|
531
|
+
const outcome = outcomeOf(tr);
|
|
532
|
+
const neutral = outcome === "not_checked" || outcome === "not_run";
|
|
533
|
+
const icon = neutral ? chalk2.gray("\u25CB") : cur === "passed" ? chalk2.green("\u2713") : cur === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
|
|
534
|
+
const label = neutral ? outcome.replace("_", " ") : cur;
|
|
499
535
|
const dur = tr.duration_ms ? ` (${(tr.duration_ms / 1e3).toFixed(1)}s)` : "";
|
|
500
|
-
printBelowSpinner(
|
|
501
|
-
|
|
536
|
+
printBelowSpinner(
|
|
537
|
+
spinner,
|
|
538
|
+
`${icon} ${chalk2.bold(tr.case_name || "Case")} ${label}${dur}`
|
|
539
|
+
);
|
|
540
|
+
if (tr.error_message && cur !== "passed" && !neutral) {
|
|
502
541
|
printBelowSpinner(spinner, ` ${chalk2.red(tr.error_message)}`);
|
|
503
542
|
}
|
|
504
543
|
}
|
|
505
544
|
caseStateBefore.set(tr.public_id, cur);
|
|
506
545
|
}
|
|
507
546
|
}
|
|
508
|
-
if (
|
|
547
|
+
if (TERMINAL_STATUSES.includes(result.status)) {
|
|
509
548
|
if (printedSteps.size > 0 && Array.isArray(result.steps)) {
|
|
510
549
|
spinner.stop();
|
|
511
550
|
result.steps.forEach((st, i) => {
|
|
@@ -555,7 +594,7 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
|
|
|
555
594
|
if (result.type === "suite_run" && Array.isArray(result.test_runs)) {
|
|
556
595
|
const total = result.total_cases ?? result.test_runs.length;
|
|
557
596
|
const done = result.test_runs.filter(
|
|
558
|
-
(tr) =>
|
|
597
|
+
(tr) => TERMINAL_STATUSES.includes(tr.status ?? "")
|
|
559
598
|
).length;
|
|
560
599
|
spinner.text = `Running suite \u2014 ${done}/${total} cases complete...`;
|
|
561
600
|
} else {
|
|
@@ -652,10 +691,10 @@ function displayProgressEvent(evt, ctx) {
|
|
|
652
691
|
break;
|
|
653
692
|
}
|
|
654
693
|
case "run_completed": {
|
|
655
|
-
const icon = evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
|
|
694
|
+
const icon = evt.status === "not_checked" ? chalk2.gray("\u25CB") : evt.status === "passed" ? chalk2.green("\u2713") : evt.status === "cancelled" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
|
|
656
695
|
spinner.stop();
|
|
657
696
|
process.stderr.write(
|
|
658
|
-
`${icon} Run ${evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
|
|
697
|
+
`${icon} Run ${evt.status === "not_checked" ? "not checked" : evt.status}${evt.duration_ms ? ` (${(evt.duration_ms / 1e3).toFixed(1)}s)` : ""}
|
|
659
698
|
`
|
|
660
699
|
);
|
|
661
700
|
resumeSpinner(spinner);
|
|
@@ -695,14 +734,30 @@ function outputResult(result, options) {
|
|
|
695
734
|
}
|
|
696
735
|
}
|
|
697
736
|
function mapExitCode(status) {
|
|
698
|
-
if (status === "passed") return EXIT_PASS;
|
|
737
|
+
if (status === "passed" || status === "not_checked") return EXIT_PASS;
|
|
699
738
|
if (status === "failed") return EXIT_TEST_FAIL;
|
|
700
739
|
return EXIT_INFRA_ERROR;
|
|
701
740
|
}
|
|
741
|
+
function isQuarantinedFailure2(result) {
|
|
742
|
+
return result.status === "failed" && result.quarantined === true;
|
|
743
|
+
}
|
|
744
|
+
function runExitCode(result) {
|
|
745
|
+
return isQuarantinedFailure2(result) ? EXIT_PASS : mapExitCode(result.status);
|
|
746
|
+
}
|
|
702
747
|
|
|
703
748
|
// src/lib/run-target.ts
|
|
704
749
|
import chalk3 from "chalk";
|
|
705
750
|
var RUN_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
|
|
751
|
+
var SUITE_FILTER_PLATFORMS = [...RUN_PLATFORMS, "roku"];
|
|
752
|
+
function parseSuitePlatformFilter(value) {
|
|
753
|
+
if (value === void 0) return void 0;
|
|
754
|
+
if (!SUITE_FILTER_PLATFORMS.includes(value)) {
|
|
755
|
+
throw new Error(
|
|
756
|
+
`--platform must be one of ${SUITE_FILTER_PLATFORMS.join(", ")}, got "${value}"`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return value;
|
|
760
|
+
}
|
|
706
761
|
function parseRunPlatform(value) {
|
|
707
762
|
if (value === void 0) return void 0;
|
|
708
763
|
if (!RUN_PLATFORMS.includes(value)) {
|
|
@@ -802,6 +857,8 @@ ${signal} received.
|
|
|
802
857
|
device: options.device,
|
|
803
858
|
build: options.build,
|
|
804
859
|
metadata: options.metadata,
|
|
860
|
+
// Opt-in: always sent, so an older server's CI default never retries.
|
|
861
|
+
retryFailed: options.retry === true,
|
|
805
862
|
platform: options.platform,
|
|
806
863
|
app: options.app
|
|
807
864
|
});
|
|
@@ -838,7 +895,7 @@ ${signal} received.
|
|
|
838
895
|
response.build_warning ? { ...result, build_warning: response.build_warning } : result,
|
|
839
896
|
options
|
|
840
897
|
);
|
|
841
|
-
process.exit(
|
|
898
|
+
process.exit(runExitCode(result));
|
|
842
899
|
} catch (err) {
|
|
843
900
|
spinner.fail(err instanceof Error ? err.message : "Unknown error");
|
|
844
901
|
outputRunError(err, options, {
|
|
@@ -928,7 +985,9 @@ ${signal} received.
|
|
|
928
985
|
device_category: opts.device ? void 0 : opts.deviceCategory,
|
|
929
986
|
device_id: opts.device,
|
|
930
987
|
build_id: opts.build,
|
|
931
|
-
metadata: opts.metadata
|
|
988
|
+
metadata: opts.metadata,
|
|
989
|
+
// Opt-in: always sent, so an older server's CI default never retries.
|
|
990
|
+
retry_failed: opts.retry === true
|
|
932
991
|
});
|
|
933
992
|
} catch (err) {
|
|
934
993
|
spinner.stop();
|
|
@@ -989,7 +1048,7 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
989
1048
|
pending = [];
|
|
990
1049
|
if (nextText !== null) spinner.start(nextText);
|
|
991
1050
|
};
|
|
992
|
-
const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled"]);
|
|
1051
|
+
const TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "cancelled", "not_checked"]);
|
|
993
1052
|
let last = null;
|
|
994
1053
|
const abandon = async (reason) => {
|
|
995
1054
|
flush(null);
|
|
@@ -1048,10 +1107,11 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
1048
1107
|
}
|
|
1049
1108
|
const wasTerminal = prev !== void 0 && TERMINAL.has(prev);
|
|
1050
1109
|
if (TERMINAL.has(cur) && !wasTerminal) {
|
|
1051
|
-
const
|
|
1110
|
+
const nothingChecked = outcomeOf(sr) === "not_checked";
|
|
1111
|
+
const mark = nothingChecked ? chalk5.gray("\u25CB") : cur === "passed" ? chalk5.green("\u2713") : cur === "cancelled" ? chalk5.yellow("\u26A0") : chalk5.red("\u2717");
|
|
1052
1112
|
const link = sr.url ? chalk5.dim(" " + sr.url) : "";
|
|
1053
1113
|
print(
|
|
1054
|
-
`${mark} ${chalk5.bold(sr.suite_name)} ${cur}: ${sr
|
|
1114
|
+
`${mark} ${chalk5.bold(sr.suite_name)} ${nothingChecked ? "not checked" : cur}: ${suiteCounts(sr)}${link}`
|
|
1055
1115
|
);
|
|
1056
1116
|
if (sr.error_message) {
|
|
1057
1117
|
print(` ${chalk5.red(sr.error_message)}`);
|
|
@@ -1105,39 +1165,48 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
1105
1165
|
0
|
|
1106
1166
|
);
|
|
1107
1167
|
const aggregateNote = totalNotChecked > 0 ? chalk5.yellow(` (${stepsNotCheckedLabel(totalNotChecked)})`) : "";
|
|
1168
|
+
const aggregate = outcomeOf(last) === "not_checked" ? "not checked" : last.status;
|
|
1108
1169
|
process.stdout.write(`
|
|
1109
|
-
Aggregate: ${
|
|
1170
|
+
Aggregate: ${aggregate}${aggregateNote}
|
|
1110
1171
|
`);
|
|
1111
1172
|
for (const sr of last.suite_runs) {
|
|
1112
|
-
const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
1173
|
+
const mark = outcomeOf(sr) === "not_checked" ? chalk5.gray("\u25CB") : sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
1113
1174
|
const link = sr.url ? chalk5.dim(` ${sr.url}`) : "";
|
|
1114
|
-
process.stdout.write(
|
|
1115
|
-
|
|
1116
|
-
`
|
|
1117
|
-
);
|
|
1175
|
+
process.stdout.write(` ${mark} ${sr.suite_name}: ${suiteCounts(sr)}${link}
|
|
1176
|
+
`);
|
|
1118
1177
|
if (sr.error_message) {
|
|
1119
1178
|
process.stdout.write(` ${chalk5.red(sr.error_message)}
|
|
1120
1179
|
`);
|
|
1121
1180
|
}
|
|
1122
1181
|
for (const cr of sr.case_runs ?? []) {
|
|
1123
|
-
const
|
|
1182
|
+
const outcome = outcomeOf(cr);
|
|
1183
|
+
const neutral = outcome === "not_checked" || outcome === "not_run";
|
|
1184
|
+
const caseMark = neutral ? chalk5.gray("\u25CB") : cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
|
|
1124
1185
|
const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
|
|
1125
|
-
const unchecked = notCheckedSuffix(
|
|
1186
|
+
const unchecked = notCheckedSuffix(outcome, cr.steps_not_checked);
|
|
1187
|
+
const label = neutral ? outcome.replace("_", " ") : cr.status ?? "?";
|
|
1126
1188
|
process.stdout.write(
|
|
1127
|
-
` ${caseMark} ${cr.case_name ?? "Case"} ${
|
|
1189
|
+
` ${caseMark} ${cr.case_name ?? "Case"} ${label}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
|
|
1128
1190
|
`
|
|
1129
1191
|
);
|
|
1130
|
-
if (cr.error_message &&
|
|
1192
|
+
if (cr.error_message && neutral) {
|
|
1193
|
+
process.stdout.write(` ${chalk5.dim(cr.error_message)}
|
|
1194
|
+
`);
|
|
1195
|
+
} else if (cr.error_message && cr.status !== "passed") {
|
|
1131
1196
|
process.stdout.write(` ${chalk5.red(cr.error_message)}
|
|
1132
1197
|
`);
|
|
1133
1198
|
}
|
|
1134
1199
|
}
|
|
1135
1200
|
}
|
|
1136
1201
|
}
|
|
1137
|
-
if (last.status === "passed") process.exit(EXIT_PASS);
|
|
1202
|
+
if (last.status === "passed" || last.status === "not_checked") process.exit(EXIT_PASS);
|
|
1138
1203
|
if (last.status === "failed") process.exit(EXIT_TEST_FAIL);
|
|
1139
1204
|
process.exit(EXIT_INFRA_ERROR);
|
|
1140
1205
|
}
|
|
1206
|
+
function suiteCounts(sr) {
|
|
1207
|
+
const nc = sr.not_checked_tests ?? 0;
|
|
1208
|
+
return `${sr.passed} passed, ${sr.failed} failed${nc > 0 ? `, ${nc} not checked` : ""}`;
|
|
1209
|
+
}
|
|
1141
1210
|
|
|
1142
1211
|
// src/commands/run-test.ts
|
|
1143
1212
|
import chalk6 from "chalk";
|
|
@@ -1206,6 +1275,8 @@ ${signal} received.
|
|
|
1206
1275
|
device: options.device,
|
|
1207
1276
|
build: options.build,
|
|
1208
1277
|
metadata: options.metadata,
|
|
1278
|
+
// Opt-in: always sent, so an older server's CI default never retries.
|
|
1279
|
+
retryFailed: options.retry === true,
|
|
1209
1280
|
platform: options.platform,
|
|
1210
1281
|
app: options.app
|
|
1211
1282
|
});
|
|
@@ -1243,7 +1314,7 @@ ${chalk6.dim("Test:")} ${testId}
|
|
|
1243
1314
|
response.build_warning ? { ...result, build_warning: response.build_warning } : result,
|
|
1244
1315
|
options
|
|
1245
1316
|
);
|
|
1246
|
-
process.exit(
|
|
1317
|
+
process.exit(runExitCode(result));
|
|
1247
1318
|
} catch (err) {
|
|
1248
1319
|
spinner.fail(err instanceof Error ? err.message : "Unknown error");
|
|
1249
1320
|
outputRunError(err, options, {
|
|
@@ -1383,11 +1454,11 @@ async function stopCommand(sessionId, opts, deps) {
|
|
|
1383
1454
|
import chalk10 from "chalk";
|
|
1384
1455
|
import { existsSync, readFileSync } from "fs";
|
|
1385
1456
|
async function readStdin() {
|
|
1386
|
-
return new Promise((
|
|
1457
|
+
return new Promise((resolve2, reject) => {
|
|
1387
1458
|
let data = "";
|
|
1388
1459
|
const timeout = setTimeout(() => {
|
|
1389
1460
|
process.stdin.removeAllListeners();
|
|
1390
|
-
|
|
1461
|
+
resolve2(data);
|
|
1391
1462
|
}, 5e3);
|
|
1392
1463
|
process.stdin.setEncoding("utf-8");
|
|
1393
1464
|
process.stdin.on("data", (chunk) => {
|
|
@@ -1395,7 +1466,7 @@ async function readStdin() {
|
|
|
1395
1466
|
});
|
|
1396
1467
|
process.stdin.on("end", () => {
|
|
1397
1468
|
clearTimeout(timeout);
|
|
1398
|
-
|
|
1469
|
+
resolve2(data);
|
|
1399
1470
|
});
|
|
1400
1471
|
process.stdin.on("error", (err) => {
|
|
1401
1472
|
clearTimeout(timeout);
|
|
@@ -1439,14 +1510,16 @@ function assertHasSteps(steps) {
|
|
|
1439
1510
|
async function resolveAppId(client, appRef, platform) {
|
|
1440
1511
|
if (appRef.includes(".")) {
|
|
1441
1512
|
if (!platform) {
|
|
1442
|
-
throw new
|
|
1513
|
+
throw new UsageError(
|
|
1443
1514
|
"When using a package name for --app, you must also specify --platform (ios, android)."
|
|
1444
1515
|
);
|
|
1445
1516
|
}
|
|
1446
1517
|
const apps = await client.listApps();
|
|
1447
1518
|
const match = apps.find((a) => a.package_name === appRef && a.platform === platform);
|
|
1448
1519
|
if (!match) {
|
|
1449
|
-
throw new
|
|
1520
|
+
throw new UsageError(
|
|
1521
|
+
`No app found with package name "${appRef}" and platform "${platform}".`
|
|
1522
|
+
);
|
|
1450
1523
|
}
|
|
1451
1524
|
return match.public_id;
|
|
1452
1525
|
}
|
|
@@ -1470,7 +1543,8 @@ async function createTest(options) {
|
|
|
1470
1543
|
steps,
|
|
1471
1544
|
context: options.context,
|
|
1472
1545
|
mode: "ai",
|
|
1473
|
-
if_exists: options.ifExists
|
|
1546
|
+
if_exists: options.ifExists,
|
|
1547
|
+
group_ids: options.group?.length ? options.group : void 0
|
|
1474
1548
|
});
|
|
1475
1549
|
spinner.succeed(
|
|
1476
1550
|
result.outcome === "updated" ? `Test updated: ${result.name}` : result.outcome === "unchanged" ? `Test already exists, unchanged: ${result.name}` : `Test created: ${result.name}`
|
|
@@ -1483,6 +1557,7 @@ async function createTest(options) {
|
|
|
1483
1557
|
name: result.name,
|
|
1484
1558
|
app_id: appPublicId || null,
|
|
1485
1559
|
outcome: result.outcome ?? null,
|
|
1560
|
+
groups: result.groups ?? [],
|
|
1486
1561
|
url: testCaseUrl(baseUrl, result.public_id)
|
|
1487
1562
|
},
|
|
1488
1563
|
null,
|
|
@@ -1498,6 +1573,9 @@ async function createTest(options) {
|
|
|
1498
1573
|
if (result.platform) {
|
|
1499
1574
|
console.log(` ${chalk10.dim(`Platform: ${result.platform}`)}`);
|
|
1500
1575
|
}
|
|
1576
|
+
if (result.groups?.length) {
|
|
1577
|
+
console.log(` ${chalk10.dim(`Groups: ${result.groups.map((g) => g.name).join(", ")}`)}`);
|
|
1578
|
+
}
|
|
1501
1579
|
console.log(` ${chalk10.dim(`Steps: ${steps.length}`)}`);
|
|
1502
1580
|
console.log(chalk10.dim(` ${testCaseUrl(baseUrl, result.public_id)}
|
|
1503
1581
|
`));
|
|
@@ -1511,6 +1589,153 @@ async function createTest(options) {
|
|
|
1511
1589
|
}
|
|
1512
1590
|
}
|
|
1513
1591
|
|
|
1592
|
+
// src/commands/import-maestro.ts
|
|
1593
|
+
import chalk11 from "chalk";
|
|
1594
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
1595
|
+
import { basename, extname, join, relative, resolve, sep } from "path";
|
|
1596
|
+
import { zipSync } from "fflate";
|
|
1597
|
+
var MAX_FOLDER_FLOWS = 200;
|
|
1598
|
+
var MAX_FLOW_BYTES = 256 * 1024;
|
|
1599
|
+
var MAX_TOTAL_BYTES = 5 * 1024 * 1024;
|
|
1600
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
1601
|
+
function readMaestroUpload(path) {
|
|
1602
|
+
if (!existsSync2(path)) throw new UsageError(`Not found: ${path}`);
|
|
1603
|
+
if (statSync(path).isFile()) {
|
|
1604
|
+
const ext = extname(path).toLowerCase();
|
|
1605
|
+
if (![".yaml", ".yml", ".zip"].includes(ext)) {
|
|
1606
|
+
throw new UsageError(
|
|
1607
|
+
"Pass a Maestro flow (.yaml or .yml), a .zip of your flows folder, or the folder itself."
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const size = statSync(path).size;
|
|
1611
|
+
const cap = ext === ".zip" ? MAX_TOTAL_BYTES : MAX_FLOW_BYTES;
|
|
1612
|
+
if (size > cap) {
|
|
1613
|
+
throw new UsageError(`${path} is larger than ${cap / 1024} KB.`);
|
|
1614
|
+
}
|
|
1615
|
+
return { name: basename(path), bytes: readFileSync2(path) };
|
|
1616
|
+
}
|
|
1617
|
+
const root = resolve(path);
|
|
1618
|
+
const entries = {};
|
|
1619
|
+
let count = 0;
|
|
1620
|
+
let total = 0;
|
|
1621
|
+
const walk = (dir) => {
|
|
1622
|
+
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
1623
|
+
if (ent.isSymbolicLink()) continue;
|
|
1624
|
+
const full = join(dir, ent.name);
|
|
1625
|
+
if (ent.isDirectory()) {
|
|
1626
|
+
if (!SKIP_DIRS.has(ent.name)) walk(full);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (!ent.isFile() || !/\.ya?ml$/i.test(ent.name)) continue;
|
|
1630
|
+
const rel = relative(root, full).split(sep).join("/");
|
|
1631
|
+
const bytes2 = readFileSync2(full);
|
|
1632
|
+
count += 1;
|
|
1633
|
+
total += bytes2.length;
|
|
1634
|
+
if (count > MAX_FOLDER_FLOWS) {
|
|
1635
|
+
throw new UsageError(`${path} holds more than ${MAX_FOLDER_FLOWS} flow files.`);
|
|
1636
|
+
}
|
|
1637
|
+
if (bytes2.length > MAX_FLOW_BYTES) {
|
|
1638
|
+
throw new UsageError(`${rel} is larger than ${MAX_FLOW_BYTES / 1024} KB.`);
|
|
1639
|
+
}
|
|
1640
|
+
if (total > MAX_TOTAL_BYTES) {
|
|
1641
|
+
throw new UsageError(`The flows in ${path} add up to more than 5 MB.`);
|
|
1642
|
+
}
|
|
1643
|
+
entries[rel] = bytes2;
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
walk(root);
|
|
1647
|
+
if (count === 0) throw new UsageError(`No .yaml or .yml flows found in ${path}.`);
|
|
1648
|
+
const bytes = zipSync(entries);
|
|
1649
|
+
if (bytes.length > MAX_TOTAL_BYTES) {
|
|
1650
|
+
throw new UsageError(`The zipped flows in ${path} are larger than 5 MB.`);
|
|
1651
|
+
}
|
|
1652
|
+
return { name: `${basename(root) || "flows"}.zip`, bytes };
|
|
1653
|
+
}
|
|
1654
|
+
var FLAG_LABEL = {
|
|
1655
|
+
rewrite: "needs rewrite",
|
|
1656
|
+
check: "check",
|
|
1657
|
+
dropped: "not imported"
|
|
1658
|
+
};
|
|
1659
|
+
var START_STATE_LABEL = {
|
|
1660
|
+
reinstall: "Fresh install",
|
|
1661
|
+
clear_data: "Clean app data",
|
|
1662
|
+
relaunch: "Restart the app",
|
|
1663
|
+
none: "Where the last test stopped"
|
|
1664
|
+
};
|
|
1665
|
+
function flagLine(f) {
|
|
1666
|
+
const where = [f.file, f.where, f.command].filter(Boolean).join(" \xB7 ");
|
|
1667
|
+
const label = f.severity === "rewrite" ? chalk11.red(FLAG_LABEL.rewrite) : FLAG_LABEL[f.severity];
|
|
1668
|
+
return ` ${label} ${chalk11.dim(where)} ${f.message}`;
|
|
1669
|
+
}
|
|
1670
|
+
function formatImport(result, baseUrl) {
|
|
1671
|
+
const lines = [];
|
|
1672
|
+
const tests = result.tests.filter((t) => t.importable);
|
|
1673
|
+
if (result.created === null) {
|
|
1674
|
+
lines.push(
|
|
1675
|
+
`Preview: ${tests.length} test(s) and ${result.setups.length} setup(s) would be created. Nothing was saved; run again without --dry-run to import.`
|
|
1676
|
+
);
|
|
1677
|
+
} else {
|
|
1678
|
+
lines.push(`Created ${result.created} test(s), setups included.`);
|
|
1679
|
+
}
|
|
1680
|
+
for (const f of result.flags) lines.push(flagLine(f));
|
|
1681
|
+
for (const t of [...result.tests, ...result.setups]) {
|
|
1682
|
+
const meta = [`${t.steps.length} ${t.steps.length === 1 ? "step" : "steps"}`];
|
|
1683
|
+
if (t.setup_keys.length) meta.push(`before this test: ${t.setup_keys.join(", ")}`);
|
|
1684
|
+
if (t.start_state) meta.push(`start: ${START_STATE_LABEL[t.start_state] ?? t.start_state}`);
|
|
1685
|
+
const head = t.importable ? t.name : chalk11.red(`${t.name} (not imported)`);
|
|
1686
|
+
lines.push("", ` ${head} ${chalk11.dim(`${t.key} \xB7 ${meta.join(" \xB7 ")}`)}`);
|
|
1687
|
+
if (t.created_id) lines.push(` ${chalk11.dim(testCaseUrl(baseUrl, t.created_id))}`);
|
|
1688
|
+
for (const f of t.flags) lines.push(flagLine(f));
|
|
1689
|
+
}
|
|
1690
|
+
if (result.test_data.length) {
|
|
1691
|
+
lines.push("", "These tests use test data:");
|
|
1692
|
+
for (const d of result.test_data) {
|
|
1693
|
+
const note = d.status === "needs_value" || d.status === "skipped" || d.suggested_value === null ? "add the value under App notes & data" : d.status === "kept" ? "already set, kept" : d.status === "created" ? "added from the flow" : d.secret ? "stored as a secret" : `flow value: ${d.suggested_value}`;
|
|
1694
|
+
lines.push(` ${d.key} ${chalk11.dim(note)}`);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return lines;
|
|
1698
|
+
}
|
|
1699
|
+
async function importMaestro(path, options) {
|
|
1700
|
+
let upload;
|
|
1701
|
+
try {
|
|
1702
|
+
if (!options.app) throw new UsageError("Pass the app to import into: --app <app id>.");
|
|
1703
|
+
upload = readMaestroUpload(path);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
if (options.output === "json") printJsonError(err);
|
|
1706
|
+
throw err;
|
|
1707
|
+
}
|
|
1708
|
+
const { token, baseUrl } = resolveAuth(options);
|
|
1709
|
+
const client = new ApiClient(baseUrl, token);
|
|
1710
|
+
try {
|
|
1711
|
+
const appId = await resolveAppId(client, options.app, options.platform);
|
|
1712
|
+
const spinner = createSpinner(options.dryRun ? "Reading flows..." : "Importing flows...", {
|
|
1713
|
+
silent: options.output === "json"
|
|
1714
|
+
});
|
|
1715
|
+
spinner.start();
|
|
1716
|
+
let result;
|
|
1717
|
+
try {
|
|
1718
|
+
result = await client.importMaestro(upload, {
|
|
1719
|
+
appId,
|
|
1720
|
+
group: options.group,
|
|
1721
|
+
dryRun: options.dryRun === true
|
|
1722
|
+
});
|
|
1723
|
+
} finally {
|
|
1724
|
+
spinner.stop();
|
|
1725
|
+
}
|
|
1726
|
+
if (options.output === "json") {
|
|
1727
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
for (const line of formatImport(result, baseUrl)) console.log(line);
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
if (options.output === "json") printJsonError(err);
|
|
1733
|
+
console.error(chalk11.red(err instanceof Error ? err.message : "Import failed"));
|
|
1734
|
+
const rejected = err instanceof UsageError || err instanceof NotFoundError || err instanceof ApiStatusError && err.status >= 400 && err.status < 500 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR;
|
|
1735
|
+
process.exit(errorExitCode(err, rejected));
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1514
1739
|
// src/commands/login.ts
|
|
1515
1740
|
import { randomUUID } from "crypto";
|
|
1516
1741
|
import open from "open";
|
|
@@ -1588,7 +1813,7 @@ async function logout() {
|
|
|
1588
1813
|
}
|
|
1589
1814
|
|
|
1590
1815
|
// src/commands/dev-status.ts
|
|
1591
|
-
import
|
|
1816
|
+
import chalk12 from "chalk";
|
|
1592
1817
|
function buildLine(info) {
|
|
1593
1818
|
if (!info || typeof info.app_build_id !== "number") return null;
|
|
1594
1819
|
const version = info.app_version ? ` version ${info.app_version}${info.app_build_number ? ` (${info.app_build_number})` : ""}` : "";
|
|
@@ -1649,7 +1874,7 @@ async function devStatus(opts) {
|
|
|
1649
1874
|
console.error(` url: ${state.page_url}`);
|
|
1650
1875
|
console.error(` started: ${state.started_at}`);
|
|
1651
1876
|
}
|
|
1652
|
-
if (metro) console.error(
|
|
1877
|
+
if (metro) console.error(chalk12.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
|
|
1653
1878
|
process.exit(0);
|
|
1654
1879
|
}
|
|
1655
1880
|
var LIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "queued", "starting", "active"]);
|
|
@@ -1674,7 +1899,7 @@ async function reportOrphan(api, state, opts) {
|
|
|
1674
1899
|
);
|
|
1675
1900
|
} else if (live) {
|
|
1676
1901
|
console.error(
|
|
1677
|
-
|
|
1902
|
+
chalk12.yellow(
|
|
1678
1903
|
`\u26A0 The dev process (pid ${state.pid}) has exited, but session ${state.session_id} is still ${status}.`
|
|
1679
1904
|
)
|
|
1680
1905
|
);
|
|
@@ -1722,12 +1947,12 @@ async function devStop(opts) {
|
|
|
1722
1947
|
}
|
|
1723
1948
|
|
|
1724
1949
|
// src/commands/dev-reload.ts
|
|
1725
|
-
import
|
|
1950
|
+
import chalk13 from "chalk";
|
|
1726
1951
|
function fail(opts, code, message, exit) {
|
|
1727
1952
|
if (opts.json) {
|
|
1728
1953
|
console.log(JSON.stringify({ reloaded: false, code, message }));
|
|
1729
1954
|
} else {
|
|
1730
|
-
console.error(
|
|
1955
|
+
console.error(chalk13.red(`error: ${message}`));
|
|
1731
1956
|
}
|
|
1732
1957
|
process.exit(exit);
|
|
1733
1958
|
}
|
|
@@ -1780,7 +2005,7 @@ async function devReload(opts) {
|
|
|
1780
2005
|
api,
|
|
1781
2006
|
// Build output and the "reloaded in" line are human output: stderr, so
|
|
1782
2007
|
// stdout stays reserved for the --json result.
|
|
1783
|
-
log: (line) => console.error(
|
|
2008
|
+
log: (line) => console.error(chalk13.dim(line))
|
|
1784
2009
|
});
|
|
1785
2010
|
const started = Date.now();
|
|
1786
2011
|
try {
|
|
@@ -1802,10 +2027,10 @@ async function devReload(opts) {
|
|
|
1802
2027
|
}
|
|
1803
2028
|
|
|
1804
2029
|
// src/commands/builds.ts
|
|
1805
|
-
import
|
|
2030
|
+
import chalk15 from "chalk";
|
|
1806
2031
|
|
|
1807
2032
|
// src/lib/formatters/builds.ts
|
|
1808
|
-
import
|
|
2033
|
+
import chalk14 from "chalk";
|
|
1809
2034
|
var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
|
|
1810
2035
|
var UNBILLED_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
|
|
1811
2036
|
function formatCostCell(costMicro, status) {
|
|
@@ -1813,13 +2038,13 @@ function formatCostCell(costMicro, status) {
|
|
|
1813
2038
|
return `$${(Math.max(costMicro, 0) / 1e6).toFixed(2)}`;
|
|
1814
2039
|
}
|
|
1815
2040
|
var STATUS_COLOR = {
|
|
1816
|
-
succeeded:
|
|
1817
|
-
failed:
|
|
1818
|
-
timed_out:
|
|
1819
|
-
infra_failed:
|
|
1820
|
-
canceled:
|
|
1821
|
-
running:
|
|
1822
|
-
queued:
|
|
2041
|
+
succeeded: chalk14.green,
|
|
2042
|
+
failed: chalk14.red,
|
|
2043
|
+
timed_out: chalk14.red,
|
|
2044
|
+
infra_failed: chalk14.red,
|
|
2045
|
+
canceled: chalk14.dim,
|
|
2046
|
+
running: chalk14.yellow,
|
|
2047
|
+
queued: chalk14.dim
|
|
1823
2048
|
};
|
|
1824
2049
|
function formatBuildsHuman(builds2) {
|
|
1825
2050
|
if (builds2.length === 0) {
|
|
@@ -1835,13 +2060,13 @@ function formatBuildsHuman(builds2) {
|
|
|
1835
2060
|
]);
|
|
1836
2061
|
const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1837
2062
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
1838
|
-
const
|
|
1839
|
-
const lines = [
|
|
2063
|
+
const join5 = (cells) => cells.join(" ").trimEnd();
|
|
2064
|
+
const lines = [chalk14.bold(join5(padCells(HEADERS)))];
|
|
1840
2065
|
for (const row of rows) {
|
|
1841
2066
|
const padded = padCells(row);
|
|
1842
2067
|
const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
|
|
1843
2068
|
padded[2] = colorFn(row[2]) + padded[2].slice(row[2].length);
|
|
1844
|
-
lines.push(
|
|
2069
|
+
lines.push(join5(padded));
|
|
1845
2070
|
}
|
|
1846
2071
|
return lines.join("\n") + "\n";
|
|
1847
2072
|
}
|
|
@@ -1895,7 +2120,7 @@ async function buildsLogs(id, opts, deps) {
|
|
|
1895
2120
|
console.log(JSON.stringify(logs));
|
|
1896
2121
|
} else {
|
|
1897
2122
|
if (logs.content) process.stdout.write(logs.content);
|
|
1898
|
-
console.error(
|
|
2123
|
+
console.error(chalk15.dim(`status: ${logs.status}`));
|
|
1899
2124
|
}
|
|
1900
2125
|
process.exitCode = 0;
|
|
1901
2126
|
return;
|
|
@@ -1992,7 +2217,7 @@ async function buildsDownload(id, opts, deps) {
|
|
|
1992
2217
|
if (opts.json) {
|
|
1993
2218
|
console.log(JSON.stringify(result));
|
|
1994
2219
|
} else {
|
|
1995
|
-
console.log(
|
|
2220
|
+
console.log(chalk15.green(`\u2713 saved ${result.path}`));
|
|
1996
2221
|
}
|
|
1997
2222
|
process.exitCode = 0;
|
|
1998
2223
|
} catch (err) {
|
|
@@ -2009,7 +2234,7 @@ async function buildsCancel(id, opts, deps) {
|
|
|
2009
2234
|
if (opts.json) {
|
|
2010
2235
|
console.log(JSON.stringify(build));
|
|
2011
2236
|
} else {
|
|
2012
|
-
console.log(`${
|
|
2237
|
+
console.log(`${chalk15.cyan(build.id)} ${build.status}`);
|
|
2013
2238
|
}
|
|
2014
2239
|
process.exitCode = 0;
|
|
2015
2240
|
} catch (err) {
|
|
@@ -2026,7 +2251,7 @@ async function buildsDeleteArtifact(id, opts, deps) {
|
|
|
2026
2251
|
if (opts.json) {
|
|
2027
2252
|
console.log(JSON.stringify(result));
|
|
2028
2253
|
} else {
|
|
2029
|
-
console.log(
|
|
2254
|
+
console.log(chalk15.green(`\u2713 deleted artifact for ${id}`));
|
|
2030
2255
|
}
|
|
2031
2256
|
process.exitCode = 0;
|
|
2032
2257
|
} catch (err) {
|
|
@@ -2037,11 +2262,11 @@ async function buildsDeleteArtifact(id, opts, deps) {
|
|
|
2037
2262
|
function printError(opts, err) {
|
|
2038
2263
|
const message = err instanceof Error ? err.message : String(err);
|
|
2039
2264
|
if (opts.json) console.log(JSON.stringify({ event: "error", message }));
|
|
2040
|
-
console.error(
|
|
2265
|
+
console.error(chalk15.red(`error: ${message}`));
|
|
2041
2266
|
}
|
|
2042
2267
|
|
|
2043
2268
|
// src/commands/submit.ts
|
|
2044
|
-
import
|
|
2269
|
+
import chalk16 from "chalk";
|
|
2045
2270
|
function defaultDeps2(client) {
|
|
2046
2271
|
return {
|
|
2047
2272
|
createSubmission: client.createSubmission.bind(client),
|
|
@@ -2065,7 +2290,7 @@ function submitConfigFor(cwd, platform) {
|
|
|
2065
2290
|
cfg = loadDevConfig(cwd);
|
|
2066
2291
|
} catch (err) {
|
|
2067
2292
|
console.error(
|
|
2068
|
-
|
|
2293
|
+
chalk16.yellow(
|
|
2069
2294
|
`warning: vibeview.json could not be read \u2014 ignoring submit defaults (${err instanceof Error ? err.message : String(err)})`
|
|
2070
2295
|
)
|
|
2071
2296
|
);
|
|
@@ -2213,12 +2438,12 @@ async function submitCommand(opts, deps) {
|
|
|
2213
2438
|
return;
|
|
2214
2439
|
}
|
|
2215
2440
|
if (!opts.json) {
|
|
2216
|
-
console.error(` ${
|
|
2441
|
+
console.error(` ${chalk16.cyan(sub.id)} ${chalk16.dim(`[${sub.platform}]`)}`);
|
|
2217
2442
|
}
|
|
2218
2443
|
const final = await followSubmission(sub, opts, d);
|
|
2219
2444
|
if (!opts.json && final.status === "succeeded" && (final.release_status ?? releaseStatus) === "draft") {
|
|
2220
2445
|
console.error(
|
|
2221
|
-
|
|
2446
|
+
chalk16.dim("release status: draft \u2014 activate it from the Play Console when ready")
|
|
2222
2447
|
);
|
|
2223
2448
|
}
|
|
2224
2449
|
process.exitCode = final.status === "succeeded" ? 0 : 1;
|
|
@@ -2249,11 +2474,11 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
|
|
|
2249
2474
|
});
|
|
2250
2475
|
emit(opts.json, { event: "preflight", ...result });
|
|
2251
2476
|
if (!opts.json) {
|
|
2252
|
-
const mark = result.ready ?
|
|
2477
|
+
const mark = result.ready ? chalk16.green("\u2713") : chalk16.red("\u2717");
|
|
2253
2478
|
console.error(`${mark} ${result.message}`);
|
|
2254
2479
|
if (result.ready) {
|
|
2255
2480
|
console.error(
|
|
2256
|
-
|
|
2481
|
+
chalk16.dim(
|
|
2257
2482
|
` next: vibeview build --cloud --production --platform ${platform}, then vibeview submit --platform ${platform} --latest`
|
|
2258
2483
|
)
|
|
2259
2484
|
);
|
|
@@ -2287,7 +2512,7 @@ async function linkedIdentifier(platform, opts, deps) {
|
|
|
2287
2512
|
}
|
|
2288
2513
|
if (!app.package_name) return void 0;
|
|
2289
2514
|
if (!opts.json) {
|
|
2290
|
-
console.error(
|
|
2515
|
+
console.error(chalk16.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
|
|
2291
2516
|
}
|
|
2292
2517
|
return app.package_name;
|
|
2293
2518
|
}
|
|
@@ -2328,14 +2553,14 @@ async function followSubmission(created, opts, deps) {
|
|
|
2328
2553
|
if (opts.json) {
|
|
2329
2554
|
emit(opts.json, { event: "status", id: created.id, status: "queued" });
|
|
2330
2555
|
} else {
|
|
2331
|
-
console.error(
|
|
2556
|
+
console.error(chalk16.dim("\u23F3 queued \u2014 waiting for a submission slot"));
|
|
2332
2557
|
}
|
|
2333
2558
|
} else if (log.status === "running" && !printedRunning) {
|
|
2334
2559
|
printedRunning = true;
|
|
2335
2560
|
if (opts.json) {
|
|
2336
2561
|
emit(opts.json, { event: "status", id: created.id, status: "running" });
|
|
2337
2562
|
} else {
|
|
2338
|
-
console.error(`${
|
|
2563
|
+
console.error(`${chalk16.cyan("\u25B6")} submitting`);
|
|
2339
2564
|
}
|
|
2340
2565
|
}
|
|
2341
2566
|
if (log.content) {
|
|
@@ -2377,7 +2602,7 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
|
|
|
2377
2602
|
});
|
|
2378
2603
|
if (!opts.json) {
|
|
2379
2604
|
const ok = final.status === "succeeded";
|
|
2380
|
-
const mark = ok ?
|
|
2605
|
+
const mark = ok ? chalk16.green("\u2713") : chalk16.red("\u2717");
|
|
2381
2606
|
const duration = formatDuration(final.started_at, final.finished_at);
|
|
2382
2607
|
let line = `${mark} ${final.status}`;
|
|
2383
2608
|
if (duration) line += ` (${duration})`;
|
|
@@ -2389,11 +2614,11 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
|
|
|
2389
2614
|
function printError2(opts, err) {
|
|
2390
2615
|
const message = err instanceof Error ? err.message : String(err);
|
|
2391
2616
|
if (opts.json) console.log(JSON.stringify({ event: "error", message }));
|
|
2392
|
-
console.error(
|
|
2617
|
+
console.error(chalk16.red(`error: ${message}`));
|
|
2393
2618
|
}
|
|
2394
2619
|
|
|
2395
2620
|
// src/commands/versions.ts
|
|
2396
|
-
import
|
|
2621
|
+
import chalk17 from "chalk";
|
|
2397
2622
|
function defaultDeps3(client) {
|
|
2398
2623
|
return {
|
|
2399
2624
|
listAppVersions: client.listAppVersions.bind(client),
|
|
@@ -2443,7 +2668,7 @@ async function versionsSet(buildNumber, opts, deps) {
|
|
|
2443
2668
|
console.log(JSON.stringify(version));
|
|
2444
2669
|
} else {
|
|
2445
2670
|
console.log(
|
|
2446
|
-
|
|
2671
|
+
chalk17.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk17.dim(
|
|
2447
2672
|
" \u2014 the next autoIncrement production build gets " + (version.build_number + 1)
|
|
2448
2673
|
)
|
|
2449
2674
|
);
|
|
@@ -2462,19 +2687,19 @@ function formatVersionsHuman(versions2) {
|
|
|
2462
2687
|
const rows = versions2.map((v) => [v.platform, String(v.build_number), v.updated_at ?? ""]);
|
|
2463
2688
|
const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2464
2689
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2465
|
-
const
|
|
2466
|
-
const lines = [
|
|
2467
|
-
for (const row of rows) lines.push(
|
|
2690
|
+
const join5 = (cells) => cells.join(" ").trimEnd();
|
|
2691
|
+
const lines = [chalk17.bold(join5(padCells(HEADERS2)))];
|
|
2692
|
+
for (const row of rows) lines.push(join5(padCells(row)));
|
|
2468
2693
|
return lines.join("\n") + "\n";
|
|
2469
2694
|
}
|
|
2470
2695
|
function printError3(opts, err) {
|
|
2471
2696
|
const message = err instanceof Error ? err.message : String(err);
|
|
2472
2697
|
if (opts.json) console.log(JSON.stringify({ event: "error", message }));
|
|
2473
|
-
console.error(
|
|
2698
|
+
console.error(chalk17.red(`error: ${message}`));
|
|
2474
2699
|
}
|
|
2475
2700
|
|
|
2476
2701
|
// src/commands/whoami.ts
|
|
2477
|
-
import
|
|
2702
|
+
import chalk18 from "chalk";
|
|
2478
2703
|
function defaultDeps4(opts) {
|
|
2479
2704
|
const { token, baseUrl } = resolveAuth(opts);
|
|
2480
2705
|
const client = new ApiClient(baseUrl, token);
|
|
@@ -2505,33 +2730,33 @@ async function whoAmI(opts, deps) {
|
|
|
2505
2730
|
process.exitCode = 0;
|
|
2506
2731
|
return;
|
|
2507
2732
|
}
|
|
2508
|
-
console.log(`${
|
|
2733
|
+
console.log(`${chalk18.bold("Logged in as")} ${me.email}`);
|
|
2509
2734
|
if (me.memberships.length === 1) {
|
|
2510
|
-
console.log(`${
|
|
2735
|
+
console.log(`${chalk18.bold("Organization")} ${me.memberships[0].organization_name}`);
|
|
2511
2736
|
} else if (me.memberships.length > 1) {
|
|
2512
|
-
console.log(
|
|
2737
|
+
console.log(chalk18.bold("Organizations"));
|
|
2513
2738
|
for (const m of me.memberships) {
|
|
2514
2739
|
console.log(` ${m.organization_name} (${m.role})`);
|
|
2515
2740
|
}
|
|
2516
2741
|
}
|
|
2517
|
-
console.log(`${
|
|
2518
|
-
console.log(`${
|
|
2519
|
-
console.log(`${
|
|
2742
|
+
console.log(`${chalk18.bold("Member role")} ${me.role ?? "\u2014"}`);
|
|
2743
|
+
console.log(`${chalk18.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
|
|
2744
|
+
console.log(`${chalk18.bold("Effective role")} ${effective ?? "\u2014"}`);
|
|
2520
2745
|
if (me.token_access) {
|
|
2521
2746
|
console.log(
|
|
2522
|
-
`${
|
|
2747
|
+
`${chalk18.bold("Access")} ${me.token_access === "ci" ? "CI only (upload builds, run tests, read results)" : "Full access"}`
|
|
2523
2748
|
);
|
|
2524
|
-
console.log(`${
|
|
2749
|
+
console.log(`${chalk18.bold("Expires")} ${describeExpiry(me.token_expires_at)}`);
|
|
2525
2750
|
}
|
|
2526
2751
|
if (me.is_superadmin) {
|
|
2527
|
-
console.log(
|
|
2752
|
+
console.log(chalk18.dim("Superadmin (platform-wide; does not raise the org role above)"));
|
|
2528
2753
|
}
|
|
2529
2754
|
if (me.role === "admin" && me.token_role && me.token_role !== "admin" && me.token_access !== "ci") {
|
|
2530
2755
|
console.log(
|
|
2531
|
-
|
|
2756
|
+
chalk18.yellow(
|
|
2532
2757
|
`
|
|
2533
|
-
Your account is admin, but this CLI token is ${me.token_role}. The effective role is the lower of the two, so admin-only commands (such as ${
|
|
2534
|
-
Create an admin API token in the dashboard and use ${
|
|
2758
|
+
Your account is admin, but this CLI token is ${me.token_role}. The effective role is the lower of the two, so admin-only commands (such as ${chalk18.bold("vibeview env set")}) will fail with a permissions error.
|
|
2759
|
+
Create an admin API token in the dashboard and use ${chalk18.bold("--token")} or ${chalk18.bold("VIBEVIEW_API_TOKEN")}.`
|
|
2535
2760
|
)
|
|
2536
2761
|
);
|
|
2537
2762
|
}
|
|
@@ -2541,15 +2766,15 @@ Create an admin API token in the dashboard and use ${chalk17.bold("--token")} or
|
|
|
2541
2766
|
if (opts.json) {
|
|
2542
2767
|
console.log(JSON.stringify({ event: "error", message }));
|
|
2543
2768
|
}
|
|
2544
|
-
console.error(
|
|
2769
|
+
console.error(chalk18.red(`error: ${message}`));
|
|
2545
2770
|
process.exitCode = errorExitCode(err);
|
|
2546
2771
|
}
|
|
2547
2772
|
}
|
|
2548
2773
|
|
|
2549
2774
|
// src/commands/env.ts
|
|
2550
2775
|
import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
|
|
2551
|
-
import { basename } from "path";
|
|
2552
|
-
import
|
|
2776
|
+
import { basename as basename2 } from "path";
|
|
2777
|
+
import chalk19 from "chalk";
|
|
2553
2778
|
import { password } from "@inquirer/prompts";
|
|
2554
2779
|
|
|
2555
2780
|
// src/lib/gitignore.ts
|
|
@@ -2605,7 +2830,7 @@ function defaultDeps5(client) {
|
|
|
2605
2830
|
isIgnored: (path) => isGitIgnored(path),
|
|
2606
2831
|
writeFile: writeSecureFile,
|
|
2607
2832
|
readFile: (path) => readFileAsync(path),
|
|
2608
|
-
warn: (message) => console.error(
|
|
2833
|
+
warn: (message) => console.error(chalk19.yellow(`warning: ${message}`))
|
|
2609
2834
|
};
|
|
2610
2835
|
}
|
|
2611
2836
|
async function envList(opts, deps) {
|
|
@@ -2636,9 +2861,9 @@ function formatEnvVarsHuman(variables) {
|
|
|
2636
2861
|
]);
|
|
2637
2862
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2638
2863
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2639
|
-
const
|
|
2640
|
-
const lines = [
|
|
2641
|
-
for (const row of rows) lines.push(
|
|
2864
|
+
const join5 = (cells) => cells.join(" ").trimEnd();
|
|
2865
|
+
const lines = [chalk19.bold(join5(padCells(HEADERS3)))];
|
|
2866
|
+
for (const row of rows) lines.push(join5(padCells(row)));
|
|
2642
2867
|
return lines.join("\n") + "\n";
|
|
2643
2868
|
}
|
|
2644
2869
|
async function envSet(name, value, opts, deps) {
|
|
@@ -2655,7 +2880,7 @@ async function envSet(name, value, opts, deps) {
|
|
|
2655
2880
|
const visibility = resolveVisibility(opts);
|
|
2656
2881
|
if (opts.file) {
|
|
2657
2882
|
const content = await d.readFile(opts.file);
|
|
2658
|
-
const filename =
|
|
2883
|
+
const filename = basename2(opts.file);
|
|
2659
2884
|
const result2 = await d.setFileVar({ name, content, filename, visibility, app: opts.app });
|
|
2660
2885
|
printSetResult(opts, result2);
|
|
2661
2886
|
process.exitCode = 0;
|
|
@@ -2687,7 +2912,7 @@ function printSetResult(opts, result) {
|
|
|
2687
2912
|
if (opts.json) {
|
|
2688
2913
|
console.log(JSON.stringify(result));
|
|
2689
2914
|
} else {
|
|
2690
|
-
console.log(
|
|
2915
|
+
console.log(chalk19.green(`\u2713 set ${result.name} (${result.visibility})`));
|
|
2691
2916
|
}
|
|
2692
2917
|
}
|
|
2693
2918
|
async function envRm(name, opts, deps) {
|
|
@@ -2697,7 +2922,7 @@ async function envRm(name, opts, deps) {
|
|
|
2697
2922
|
if (opts.json) {
|
|
2698
2923
|
console.log(JSON.stringify({ event: "deleted", name }));
|
|
2699
2924
|
} else {
|
|
2700
|
-
console.log(
|
|
2925
|
+
console.log(chalk19.green(`\u2713 removed ${name}`));
|
|
2701
2926
|
}
|
|
2702
2927
|
process.exitCode = 0;
|
|
2703
2928
|
} catch (err) {
|
|
@@ -2729,7 +2954,7 @@ async function envPull(opts, deps) {
|
|
|
2729
2954
|
if (opts.json) {
|
|
2730
2955
|
console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
|
|
2731
2956
|
} else {
|
|
2732
|
-
console.log(
|
|
2957
|
+
console.log(chalk19.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
|
|
2733
2958
|
}
|
|
2734
2959
|
process.exitCode = 0;
|
|
2735
2960
|
} catch (err) {
|
|
@@ -2768,7 +2993,7 @@ function escapeEnvValue(value) {
|
|
|
2768
2993
|
function printError4(opts, err) {
|
|
2769
2994
|
const message = err instanceof Error ? err.message : String(err);
|
|
2770
2995
|
if (opts.json) console.log(JSON.stringify({ event: "error", message }));
|
|
2771
|
-
console.error(
|
|
2996
|
+
console.error(chalk19.red(`error: ${message}`));
|
|
2772
2997
|
}
|
|
2773
2998
|
|
|
2774
2999
|
// src/commands/credentials.ts
|
|
@@ -2779,8 +3004,8 @@ import {
|
|
|
2779
3004
|
writeFile as writeFileAsync
|
|
2780
3005
|
} from "fs/promises";
|
|
2781
3006
|
import { mkdirSync } from "fs";
|
|
2782
|
-
import { basename as
|
|
2783
|
-
import
|
|
3007
|
+
import { basename as basename3, extname as extname2, join as join2 } from "path";
|
|
3008
|
+
import chalk20 from "chalk";
|
|
2784
3009
|
import { input, password as password2 } from "@inquirer/prompts";
|
|
2785
3010
|
function clientFor2(opts) {
|
|
2786
3011
|
const { token, baseUrl } = resolveAuth(opts);
|
|
@@ -2853,7 +3078,7 @@ function defaultDeps6(client) {
|
|
|
2853
3078
|
mkdir: (dir) => mkdirSync(dir, { recursive: true }),
|
|
2854
3079
|
writeFile: (path, data, mode) => writeFileAsync(path, data, mode !== void 0 ? { mode } : void 0),
|
|
2855
3080
|
chmod: (path, mode) => chmodAsync(path, mode),
|
|
2856
|
-
warn: (message) => console.error(
|
|
3081
|
+
warn: (message) => console.error(chalk20.yellow(`warning: ${message}`))
|
|
2857
3082
|
};
|
|
2858
3083
|
}
|
|
2859
3084
|
var ANDROID_KIND = "android_keystore";
|
|
@@ -2883,10 +3108,10 @@ async function credentialsAdd(opts, deps) {
|
|
|
2883
3108
|
"a password is required: run interactively to be prompted (passwords are never accepted as flags)."
|
|
2884
3109
|
);
|
|
2885
3110
|
}
|
|
2886
|
-
const name = opts.name ??
|
|
3111
|
+
const name = opts.name ?? basename3(opts.file, extname2(opts.file));
|
|
2887
3112
|
try {
|
|
2888
3113
|
const file = await d.readFile(opts.file);
|
|
2889
|
-
const filename =
|
|
3114
|
+
const filename = basename3(opts.file);
|
|
2890
3115
|
let body;
|
|
2891
3116
|
if (platform === "android") {
|
|
2892
3117
|
const storePassword = await d.prompt({ message: "Keystore (store) password", hidden: true });
|
|
@@ -2915,14 +3140,14 @@ async function credentialsAdd(opts, deps) {
|
|
|
2915
3140
|
file,
|
|
2916
3141
|
filename,
|
|
2917
3142
|
profile,
|
|
2918
|
-
profileFilename:
|
|
3143
|
+
profileFilename: basename3(opts.profile)
|
|
2919
3144
|
};
|
|
2920
3145
|
}
|
|
2921
3146
|
const result = await d.addCredential(body);
|
|
2922
3147
|
if (opts.json) {
|
|
2923
3148
|
console.log(JSON.stringify(result));
|
|
2924
3149
|
} else {
|
|
2925
|
-
console.log(
|
|
3150
|
+
console.log(chalk20.green(`\u2713 added ${result.name} (${result.platform})`));
|
|
2926
3151
|
}
|
|
2927
3152
|
process.exitCode = 0;
|
|
2928
3153
|
} catch (err) {
|
|
@@ -2948,7 +3173,7 @@ async function credentialsGenerate(opts, deps) {
|
|
|
2948
3173
|
const outDir = opts.out ?? process.cwd();
|
|
2949
3174
|
try {
|
|
2950
3175
|
d.mkdir(outDir);
|
|
2951
|
-
const expectedPath =
|
|
3176
|
+
const expectedPath = join2(outDir, `${opts.name}.keystore`);
|
|
2952
3177
|
if (await d.fileExists(expectedPath)) {
|
|
2953
3178
|
throw new Error(
|
|
2954
3179
|
`${expectedPath} already exists \u2014 refusing to overwrite it. Move it away or pass a different --out/--name.`
|
|
@@ -2963,7 +3188,7 @@ async function credentialsGenerate(opts, deps) {
|
|
|
2963
3188
|
// charge, so a stale CLI can never accidentally opt out of it.
|
|
2964
3189
|
...opts.force ? { overwrite: true } : {}
|
|
2965
3190
|
});
|
|
2966
|
-
const savedTo =
|
|
3191
|
+
const savedTo = join2(outDir, basename3(result.filename));
|
|
2967
3192
|
if (opts.json) {
|
|
2968
3193
|
console.log(JSON.stringify({ ...result, saved_to: savedTo }));
|
|
2969
3194
|
}
|
|
@@ -2980,20 +3205,20 @@ async function credentialsGenerate(opts, deps) {
|
|
|
2980
3205
|
}
|
|
2981
3206
|
if (opts.json) {
|
|
2982
3207
|
} else {
|
|
2983
|
-
console.log(
|
|
3208
|
+
console.log(chalk20.green(`\u2713 generated ${result.name} (android)`));
|
|
2984
3209
|
console.log("");
|
|
2985
|
-
console.log(` Keystore saved to: ${
|
|
2986
|
-
console.log(` Key alias: ${
|
|
2987
|
-
console.log(` Store password: ${
|
|
2988
|
-
console.log(` Key password: ${
|
|
3210
|
+
console.log(` Keystore saved to: ${chalk20.bold(savedTo)}`);
|
|
3211
|
+
console.log(` Key alias: ${chalk20.bold(result.key_alias)}`);
|
|
3212
|
+
console.log(` Store password: ${chalk20.bold(result.store_password)}`);
|
|
3213
|
+
console.log(` Key password: ${chalk20.bold(result.key_password)}`);
|
|
2989
3214
|
console.log("");
|
|
2990
3215
|
console.log(
|
|
2991
|
-
|
|
3216
|
+
chalk20.yellow(
|
|
2992
3217
|
" These are shown ONCE \u2014 VibeView cannot recover the keystore file or its passwords. Back them up somewhere safe."
|
|
2993
3218
|
)
|
|
2994
3219
|
);
|
|
2995
3220
|
console.log(
|
|
2996
|
-
|
|
3221
|
+
chalk20.yellow(
|
|
2997
3222
|
" The credential is already uploaded and active for android and androidtv production builds."
|
|
2998
3223
|
)
|
|
2999
3224
|
);
|
|
@@ -3019,19 +3244,19 @@ async function generateAppleCredential(platform, opts, d) {
|
|
|
3019
3244
|
if (opts.json) {
|
|
3020
3245
|
console.log(JSON.stringify(result));
|
|
3021
3246
|
} else {
|
|
3022
|
-
console.log(
|
|
3247
|
+
console.log(chalk20.green(`\u2713 generated ${result.name} (${result.platform})`));
|
|
3023
3248
|
console.log("");
|
|
3024
|
-
console.log(` Scope: ${
|
|
3249
|
+
console.log(` Scope: ${chalk20.bold(result.app_public_id ?? "org-wide")}`);
|
|
3025
3250
|
const expires = formatExpires(result);
|
|
3026
3251
|
if (expires) {
|
|
3027
|
-
console.log(` Expires: ${
|
|
3252
|
+
console.log(` Expires: ${chalk20.bold(expires)}`);
|
|
3028
3253
|
}
|
|
3029
3254
|
console.log("");
|
|
3030
3255
|
console.log(
|
|
3031
3256
|
` The certificate and provisioning profile are stored with VibeView and already active for ${result.platform} production builds.`
|
|
3032
3257
|
);
|
|
3033
3258
|
console.log(
|
|
3034
|
-
` Run ${
|
|
3259
|
+
` Run ${chalk20.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
|
|
3035
3260
|
);
|
|
3036
3261
|
}
|
|
3037
3262
|
process.exitCode = 0;
|
|
@@ -3072,8 +3297,8 @@ async function credentialsDownload(name, opts, deps) {
|
|
|
3072
3297
|
}
|
|
3073
3298
|
throw err;
|
|
3074
3299
|
}
|
|
3075
|
-
const savedTo =
|
|
3076
|
-
const profileSavedTo = material.profile_base64 != null ?
|
|
3300
|
+
const savedTo = join2(outDir, basename3(material.filename));
|
|
3301
|
+
const profileSavedTo = material.profile_base64 != null ? join2(outDir, `${basename3(material.filename, extname2(material.filename))}.mobileprovision`) : null;
|
|
3077
3302
|
if (opts.json) {
|
|
3078
3303
|
console.log(
|
|
3079
3304
|
JSON.stringify({
|
|
@@ -3097,17 +3322,17 @@ async function credentialsDownload(name, opts, deps) {
|
|
|
3097
3322
|
await d.chmod(path, 384);
|
|
3098
3323
|
}
|
|
3099
3324
|
if (!opts.json) {
|
|
3100
|
-
console.log(
|
|
3325
|
+
console.log(chalk20.green(`\u2713 downloaded ${material.name} (${material.platform})`));
|
|
3101
3326
|
console.log("");
|
|
3102
|
-
console.log(` Saved to: ${
|
|
3327
|
+
console.log(` Saved to: ${chalk20.bold(savedTo)}`);
|
|
3103
3328
|
if (profileSavedTo !== null) {
|
|
3104
|
-
console.log(` Profile: ${
|
|
3329
|
+
console.log(` Profile: ${chalk20.bold(profileSavedTo)}`);
|
|
3105
3330
|
}
|
|
3106
3331
|
for (const [key, value] of Object.entries(material.passwords)) {
|
|
3107
|
-
console.log(` ${key}: ${
|
|
3332
|
+
console.log(` ${key}: ${chalk20.bold(value)}`);
|
|
3108
3333
|
}
|
|
3109
3334
|
console.log("");
|
|
3110
|
-
console.log(
|
|
3335
|
+
console.log(chalk20.yellow(" This download is recorded in your organization's audit log."));
|
|
3111
3336
|
}
|
|
3112
3337
|
process.exitCode = 0;
|
|
3113
3338
|
} catch (err) {
|
|
@@ -3133,18 +3358,18 @@ async function ascKeyAdd(opts, deps) {
|
|
|
3133
3358
|
keyId: opts.keyId,
|
|
3134
3359
|
issuerId: opts.issuerId,
|
|
3135
3360
|
file,
|
|
3136
|
-
filename:
|
|
3361
|
+
filename: basename3(opts.file)
|
|
3137
3362
|
});
|
|
3138
3363
|
if (opts.json) {
|
|
3139
3364
|
console.log(JSON.stringify(result));
|
|
3140
3365
|
} else {
|
|
3141
|
-
console.log(
|
|
3366
|
+
console.log(chalk20.green(`\u2713 added App Store Connect key ${result.name}`));
|
|
3142
3367
|
console.log("");
|
|
3143
|
-
console.log(` Key ID: ${
|
|
3144
|
-
console.log(` Issuer ID: ${
|
|
3368
|
+
console.log(` Key ID: ${chalk20.bold(result.key_id)}`);
|
|
3369
|
+
console.log(` Issuer ID: ${chalk20.bold(result.issuer_id)}`);
|
|
3145
3370
|
console.log("");
|
|
3146
3371
|
console.log(
|
|
3147
|
-
|
|
3372
|
+
chalk20.yellow(
|
|
3148
3373
|
" The .p8 is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
|
|
3149
3374
|
)
|
|
3150
3375
|
);
|
|
@@ -3178,9 +3403,9 @@ function formatAscKeysHuman(keys) {
|
|
|
3178
3403
|
const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, formatUtcTimestamp(k.created_at)]);
|
|
3179
3404
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
3180
3405
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
3181
|
-
const
|
|
3182
|
-
const lines = [
|
|
3183
|
-
for (const row of rows) lines.push(
|
|
3406
|
+
const join5 = (cells) => cells.join(" ").trimEnd();
|
|
3407
|
+
const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
|
|
3408
|
+
for (const row of rows) lines.push(join5(padCells(row)));
|
|
3184
3409
|
return lines.join("\n") + "\n";
|
|
3185
3410
|
}
|
|
3186
3411
|
async function ascKeyRm(name, opts, deps) {
|
|
@@ -3190,7 +3415,7 @@ async function ascKeyRm(name, opts, deps) {
|
|
|
3190
3415
|
if (opts.json) {
|
|
3191
3416
|
console.log(JSON.stringify({ event: "deleted", name }));
|
|
3192
3417
|
} else {
|
|
3193
|
-
console.log(
|
|
3418
|
+
console.log(chalk20.green(`\u2713 removed App Store Connect key ${name}`));
|
|
3194
3419
|
}
|
|
3195
3420
|
process.exitCode = 0;
|
|
3196
3421
|
} catch (err) {
|
|
@@ -3210,17 +3435,17 @@ async function playKeyAdd(opts, deps) {
|
|
|
3210
3435
|
const result = await d.addPlayKey({
|
|
3211
3436
|
...opts.name !== void 0 ? { name: opts.name } : {},
|
|
3212
3437
|
file,
|
|
3213
|
-
filename:
|
|
3438
|
+
filename: basename3(opts.file)
|
|
3214
3439
|
});
|
|
3215
3440
|
if (opts.json) {
|
|
3216
3441
|
console.log(JSON.stringify(result));
|
|
3217
3442
|
} else {
|
|
3218
|
-
console.log(
|
|
3443
|
+
console.log(chalk20.green(`\u2713 added Google Play key ${result.name}`));
|
|
3219
3444
|
console.log("");
|
|
3220
|
-
console.log(` Service account: ${
|
|
3445
|
+
console.log(` Service account: ${chalk20.bold(result.client_email)}`);
|
|
3221
3446
|
console.log("");
|
|
3222
3447
|
console.log(
|
|
3223
|
-
|
|
3448
|
+
chalk20.yellow(
|
|
3224
3449
|
" The JSON key is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
|
|
3225
3450
|
)
|
|
3226
3451
|
);
|
|
@@ -3254,9 +3479,9 @@ function formatPlayKeysHuman(keys) {
|
|
|
3254
3479
|
const rows = keys.map((k) => [k.name, k.client_email, formatUtcTimestamp(k.created_at)]);
|
|
3255
3480
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
3256
3481
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
3257
|
-
const
|
|
3258
|
-
const lines = [
|
|
3259
|
-
for (const row of rows) lines.push(
|
|
3482
|
+
const join5 = (cells) => cells.join(" ").trimEnd();
|
|
3483
|
+
const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
|
|
3484
|
+
for (const row of rows) lines.push(join5(padCells(row)));
|
|
3260
3485
|
return lines.join("\n") + "\n";
|
|
3261
3486
|
}
|
|
3262
3487
|
async function playKeyRm(name, opts, deps) {
|
|
@@ -3266,7 +3491,7 @@ async function playKeyRm(name, opts, deps) {
|
|
|
3266
3491
|
if (opts.json) {
|
|
3267
3492
|
console.log(JSON.stringify({ event: "deleted", name }));
|
|
3268
3493
|
} else {
|
|
3269
|
-
console.log(
|
|
3494
|
+
console.log(chalk20.green(`\u2713 removed Google Play key ${name}`));
|
|
3270
3495
|
}
|
|
3271
3496
|
process.exitCode = 0;
|
|
3272
3497
|
} catch (err) {
|
|
@@ -3334,9 +3559,9 @@ function formatCredentialsHuman(credentials2) {
|
|
|
3334
3559
|
]);
|
|
3335
3560
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
3336
3561
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
3337
|
-
const
|
|
3338
|
-
const lines = [
|
|
3339
|
-
for (const row of rows) lines.push(
|
|
3562
|
+
const join5 = (cells) => cells.join(" ").trimEnd();
|
|
3563
|
+
const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
|
|
3564
|
+
for (const row of rows) lines.push(join5(padCells(row)));
|
|
3340
3565
|
return lines.join("\n") + "\n";
|
|
3341
3566
|
}
|
|
3342
3567
|
async function credentialsRm(name, opts, deps) {
|
|
@@ -3346,7 +3571,7 @@ async function credentialsRm(name, opts, deps) {
|
|
|
3346
3571
|
if (opts.json) {
|
|
3347
3572
|
console.log(JSON.stringify({ event: "deleted", name }));
|
|
3348
3573
|
} else {
|
|
3349
|
-
console.log(
|
|
3574
|
+
console.log(chalk20.green(`\u2713 removed ${name}`));
|
|
3350
3575
|
}
|
|
3351
3576
|
process.exitCode = 0;
|
|
3352
3577
|
} catch (err) {
|
|
@@ -3358,16 +3583,16 @@ function printKeystoreWriteFailure(opts, err, result) {
|
|
|
3358
3583
|
const message = err instanceof Error ? err.message : String(err);
|
|
3359
3584
|
if (opts.json) {
|
|
3360
3585
|
console.log(JSON.stringify({ event: "error", message }));
|
|
3361
|
-
console.error(
|
|
3586
|
+
console.error(chalk20.red(`error: ${message}`));
|
|
3362
3587
|
return;
|
|
3363
3588
|
}
|
|
3364
|
-
console.error(
|
|
3589
|
+
console.error(chalk20.red(`error: ${message}`));
|
|
3365
3590
|
console.error(
|
|
3366
|
-
|
|
3591
|
+
chalk20.red(
|
|
3367
3592
|
`The keystore could NOT be saved locally, but the credential "${result.name}" WAS created server-side and its key material can never be re-fetched.`
|
|
3368
3593
|
)
|
|
3369
3594
|
);
|
|
3370
|
-
console.error(
|
|
3595
|
+
console.error(chalk20.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
|
|
3371
3596
|
console.error("");
|
|
3372
3597
|
console.error(` Key alias: ${result.key_alias}`);
|
|
3373
3598
|
console.error(` Store password: ${result.store_password}`);
|
|
@@ -3381,11 +3606,11 @@ function printKeystoreWriteFailure(opts, err, result) {
|
|
|
3381
3606
|
function printError5(opts, err) {
|
|
3382
3607
|
const message = err instanceof Error ? err.message : String(err);
|
|
3383
3608
|
if (opts.json) console.log(JSON.stringify({ event: "error", message }));
|
|
3384
|
-
console.error(
|
|
3609
|
+
console.error(chalk20.red(`error: ${message}`));
|
|
3385
3610
|
}
|
|
3386
3611
|
|
|
3387
3612
|
// src/commands/webhooks.ts
|
|
3388
|
-
import
|
|
3613
|
+
import chalk21 from "chalk";
|
|
3389
3614
|
function clientFor3(opts) {
|
|
3390
3615
|
const { token, baseUrl } = resolveAuth(opts);
|
|
3391
3616
|
return new ApiClient(baseUrl, token);
|
|
@@ -3416,14 +3641,14 @@ async function webhooksAdd(opts, deps) {
|
|
|
3416
3641
|
if (opts.json) {
|
|
3417
3642
|
console.log(JSON.stringify(result));
|
|
3418
3643
|
} else {
|
|
3419
|
-
console.log(
|
|
3644
|
+
console.log(chalk21.green(`\u2713 added ${result.id} ${result.url}`));
|
|
3420
3645
|
console.log("");
|
|
3421
|
-
console.log(` Signing secret: ${
|
|
3646
|
+
console.log(` Signing secret: ${chalk21.bold(result.secret)}`);
|
|
3422
3647
|
if (opts.secret !== void 0) {
|
|
3423
|
-
console.log(
|
|
3648
|
+
console.log(chalk21.yellow(" (This is the secret you provided.)"));
|
|
3424
3649
|
}
|
|
3425
3650
|
console.log(
|
|
3426
|
-
|
|
3651
|
+
chalk21.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
|
|
3427
3652
|
);
|
|
3428
3653
|
}
|
|
3429
3654
|
process.exitCode = 0;
|
|
@@ -3453,9 +3678,9 @@ function formatWebhooksHuman(webhooks2) {
|
|
|
3453
3678
|
]);
|
|
3454
3679
|
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
3455
3680
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
3456
|
-
const
|
|
3457
|
-
const lines = [
|
|
3458
|
-
for (const row of rows) lines.push(
|
|
3681
|
+
const join5 = (cells) => cells.join(" ").trimEnd();
|
|
3682
|
+
const lines = [chalk21.bold(join5(padCells(HEADERS3)))];
|
|
3683
|
+
for (const row of rows) lines.push(join5(padCells(row)));
|
|
3459
3684
|
return lines.join("\n") + "\n";
|
|
3460
3685
|
}
|
|
3461
3686
|
async function webhooksList(opts, deps) {
|
|
@@ -3480,7 +3705,7 @@ async function webhooksRm(id, opts, deps) {
|
|
|
3480
3705
|
if (opts.json) {
|
|
3481
3706
|
console.log(JSON.stringify({ event: "deleted", id }));
|
|
3482
3707
|
} else {
|
|
3483
|
-
console.log(
|
|
3708
|
+
console.log(chalk21.green(`\u2713 removed ${id}`));
|
|
3484
3709
|
}
|
|
3485
3710
|
process.exitCode = 0;
|
|
3486
3711
|
} catch (err) {
|
|
@@ -3513,13 +3738,13 @@ async function webhooksTest(id, opts, deps) {
|
|
|
3513
3738
|
if (opts.json) console.log(JSON.stringify(ld));
|
|
3514
3739
|
if (ld.status === "succeeded") {
|
|
3515
3740
|
if (!opts.json) {
|
|
3516
|
-
console.log(
|
|
3741
|
+
console.log(chalk21.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
|
|
3517
3742
|
}
|
|
3518
3743
|
process.exitCode = 0;
|
|
3519
3744
|
} else {
|
|
3520
3745
|
if (!opts.json) {
|
|
3521
3746
|
const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
|
|
3522
|
-
console.error(
|
|
3747
|
+
console.error(chalk21.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
|
|
3523
3748
|
}
|
|
3524
3749
|
process.exitCode = 1;
|
|
3525
3750
|
}
|
|
@@ -3528,7 +3753,7 @@ async function webhooksTest(id, opts, deps) {
|
|
|
3528
3753
|
}
|
|
3529
3754
|
if (opts.json) console.log(JSON.stringify({ status: "pending" }));
|
|
3530
3755
|
console.error(
|
|
3531
|
-
|
|
3756
|
+
chalk21.yellow(
|
|
3532
3757
|
`Delivery still pending after ${TEST_POLL_BUDGET_MS / 1e3}s \u2014 check \`vibeview webhooks list\` later for the result.`
|
|
3533
3758
|
)
|
|
3534
3759
|
);
|
|
@@ -3541,7 +3766,7 @@ async function webhooksTest(id, opts, deps) {
|
|
|
3541
3766
|
function printError6(opts, err) {
|
|
3542
3767
|
const message = err instanceof Error ? err.message : String(err);
|
|
3543
3768
|
if (opts.json) console.log(JSON.stringify({ event: "error", message }));
|
|
3544
|
-
console.error(
|
|
3769
|
+
console.error(chalk21.red(`error: ${message}`));
|
|
3545
3770
|
}
|
|
3546
3771
|
|
|
3547
3772
|
// src/commands/list-devices.ts
|
|
@@ -3582,7 +3807,7 @@ async function listDevicesCommand(options) {
|
|
|
3582
3807
|
}
|
|
3583
3808
|
|
|
3584
3809
|
// src/lib/metadata.ts
|
|
3585
|
-
import { readFileSync as
|
|
3810
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3586
3811
|
var MAX_METADATA_BYTES = 16384;
|
|
3587
3812
|
var MAX_METADATA_DEPTH = 4;
|
|
3588
3813
|
function measureDepth(value, current = 1) {
|
|
@@ -3621,7 +3846,7 @@ function parseMetadata(raw) {
|
|
|
3621
3846
|
function readMetadataFile(path) {
|
|
3622
3847
|
let raw;
|
|
3623
3848
|
try {
|
|
3624
|
-
raw =
|
|
3849
|
+
raw = readFileSync3(path, "utf8");
|
|
3625
3850
|
} catch (e) {
|
|
3626
3851
|
throw new Error(`failed to read --metadata-file ${path}: ${e.message}`);
|
|
3627
3852
|
}
|
|
@@ -3633,27 +3858,27 @@ function readMetadataFile(path) {
|
|
|
3633
3858
|
}
|
|
3634
3859
|
|
|
3635
3860
|
// src/commands/agent-setup.ts
|
|
3636
|
-
import { copyFileSync, existsSync as
|
|
3861
|
+
import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
3637
3862
|
import { homedir } from "os";
|
|
3638
|
-
import { dirname, join as
|
|
3863
|
+
import { dirname, join as join3 } from "path";
|
|
3639
3864
|
import { fileURLToPath } from "url";
|
|
3640
3865
|
import { spawnSync } from "child_process";
|
|
3641
3866
|
import { confirm } from "@inquirer/prompts";
|
|
3642
|
-
import
|
|
3867
|
+
import chalk22 from "chalk";
|
|
3643
3868
|
function resolveShippedSkillPath(fromDir) {
|
|
3644
|
-
for (const rel of ["..",
|
|
3645
|
-
const candidate =
|
|
3646
|
-
if (
|
|
3869
|
+
for (const rel of ["..", join3("..", "..")]) {
|
|
3870
|
+
const candidate = join3(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
|
|
3871
|
+
if (existsSync3(candidate)) return candidate;
|
|
3647
3872
|
}
|
|
3648
3873
|
return null;
|
|
3649
3874
|
}
|
|
3650
3875
|
function skillInstallDir(project, cwd, home) {
|
|
3651
3876
|
const base = project ? cwd : home;
|
|
3652
|
-
return
|
|
3877
|
+
return join3(base, ".claude", "skills", "vibeview-agent");
|
|
3653
3878
|
}
|
|
3654
3879
|
function installSkill(shippedPath, targetDir) {
|
|
3655
|
-
const target =
|
|
3656
|
-
const existed =
|
|
3880
|
+
const target = join3(targetDir, "SKILL.md");
|
|
3881
|
+
const existed = existsSync3(target);
|
|
3657
3882
|
mkdirSync2(targetDir, { recursive: true });
|
|
3658
3883
|
copyFileSync(shippedPath, target);
|
|
3659
3884
|
return existed ? "updated" : "installed";
|
|
@@ -3669,23 +3894,23 @@ function registerMcp() {
|
|
|
3669
3894
|
encoding: "utf8"
|
|
3670
3895
|
});
|
|
3671
3896
|
if (res.status === 0) {
|
|
3672
|
-
console.log(
|
|
3897
|
+
console.log(chalk22.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
|
|
3673
3898
|
return;
|
|
3674
3899
|
}
|
|
3675
3900
|
const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
|
|
3676
3901
|
if (out.includes("already exists")) {
|
|
3677
|
-
console.log(
|
|
3902
|
+
console.log(chalk22.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
|
|
3678
3903
|
return;
|
|
3679
3904
|
}
|
|
3680
|
-
console.error(
|
|
3681
|
-
console.error(
|
|
3905
|
+
console.error(chalk22.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
|
|
3906
|
+
console.error(chalk22.yellow(` register it manually: ${MCP_ADD_HINT}`));
|
|
3682
3907
|
}
|
|
3683
3908
|
async function agentSetup(opts) {
|
|
3684
3909
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
3685
3910
|
const shipped = resolveShippedSkillPath(moduleDir);
|
|
3686
3911
|
if (!shipped) {
|
|
3687
3912
|
console.error(
|
|
3688
|
-
|
|
3913
|
+
chalk22.red(
|
|
3689
3914
|
"error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
|
|
3690
3915
|
)
|
|
3691
3916
|
);
|
|
@@ -3694,9 +3919,9 @@ async function agentSetup(opts) {
|
|
|
3694
3919
|
}
|
|
3695
3920
|
const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
|
|
3696
3921
|
const result = installSkill(shipped, targetDir);
|
|
3697
|
-
console.log(
|
|
3922
|
+
console.log(chalk22.green(`\u2713 vibeview-agent skill ${result}: ${join3(targetDir, "SKILL.md")}`));
|
|
3698
3923
|
console.log(
|
|
3699
|
-
|
|
3924
|
+
chalk22.dim(
|
|
3700
3925
|
opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
|
|
3701
3926
|
)
|
|
3702
3927
|
);
|
|
@@ -3704,7 +3929,7 @@ async function agentSetup(opts) {
|
|
|
3704
3929
|
if (!claudeCliAvailable()) {
|
|
3705
3930
|
if (opts.mcp === true) {
|
|
3706
3931
|
console.error(
|
|
3707
|
-
|
|
3932
|
+
chalk22.yellow(`\u26A0 claude CLI not found \u2014 install Claude Code, then run: ${MCP_ADD_HINT}`)
|
|
3708
3933
|
);
|
|
3709
3934
|
}
|
|
3710
3935
|
} else if (opts.mcp === true || process.stdin.isTTY && await confirm({
|
|
@@ -3716,7 +3941,7 @@ async function agentSetup(opts) {
|
|
|
3716
3941
|
}
|
|
3717
3942
|
console.log(
|
|
3718
3943
|
`
|
|
3719
|
-
Suggested line for your project's ${
|
|
3944
|
+
Suggested line for your project's ${chalk22.bold("CLAUDE.md")} (or ${chalk22.bold(
|
|
3720
3945
|
"AGENTS.md"
|
|
3721
3946
|
)} for other agents):
|
|
3722
3947
|
`
|
|
@@ -3724,17 +3949,17 @@ Suggested line for your project's ${chalk21.bold("CLAUDE.md")} (or ${chalk21.bol
|
|
|
3724
3949
|
console.log(` ${CLAUDE_MD_SUGGESTION}
|
|
3725
3950
|
`);
|
|
3726
3951
|
console.log(
|
|
3727
|
-
|
|
3952
|
+
chalk22.dim(
|
|
3728
3953
|
"Other agents (Cursor, Copilot, Codex, ...): install the same skill from GitHub with\n npx skills add vibeview/skills\nor point them at the skill file above, or add the MCP server per client \u2014\nsee https://vibeview.io/docs/agent-control"
|
|
3729
3954
|
)
|
|
3730
3955
|
);
|
|
3731
3956
|
}
|
|
3732
3957
|
|
|
3733
3958
|
// src/lib/update-check.ts
|
|
3734
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
3959
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
3735
3960
|
import { homedir as homedir2 } from "os";
|
|
3736
|
-
import { dirname as dirname2, join as
|
|
3737
|
-
var CACHE_FILE =
|
|
3961
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
3962
|
+
var CACHE_FILE = join4(homedir2(), ".vibeview", "update-check.json");
|
|
3738
3963
|
var REGISTRY_URL = "https://registry.npmjs.org/vibeview/latest";
|
|
3739
3964
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3740
3965
|
function isNewerVersion(a, b) {
|
|
@@ -3765,7 +3990,7 @@ function updateNotice(current, latest) {
|
|
|
3765
3990
|
}
|
|
3766
3991
|
function readCache(file) {
|
|
3767
3992
|
try {
|
|
3768
|
-
const parsed = JSON.parse(
|
|
3993
|
+
const parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
3769
3994
|
if (typeof parsed?.checked_at === "number" && typeof parsed?.latest === "string") {
|
|
3770
3995
|
return parsed;
|
|
3771
3996
|
}
|
|
@@ -3848,11 +4073,14 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
|
|
|
3848
4073
|
"Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
|
|
3849
4074
|
).option(
|
|
3850
4075
|
"--platform <platform>",
|
|
3851
|
-
"Run the suite on another platform (ios, android, tvos, androidtv): runs your app
|
|
4076
|
+
"Run the suite on another platform (ios, android, tvos, androidtv): runs your linked app on that platform, else the app with the same bundle id / package"
|
|
3852
4077
|
).option(
|
|
3853
4078
|
"--app <app-id>",
|
|
3854
4079
|
"Run the suite against this app instead of its own (e.g. another platform build with a different bundle id)"
|
|
3855
|
-
).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").
|
|
4080
|
+
).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
|
|
4081
|
+
"--retry",
|
|
4082
|
+
"Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
|
|
4083
|
+
).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((suiteId, opts) => {
|
|
3856
4084
|
if (opts.device && opts.deviceCategory) {
|
|
3857
4085
|
process.stderr.write("error: --device cannot be combined with --device-category\n");
|
|
3858
4086
|
process.exit(2);
|
|
@@ -3880,7 +4108,7 @@ program.command("run-suites").description("Run all suites matching a tag filter"
|
|
|
3880
4108
|
[]
|
|
3881
4109
|
).option("--match <mode>", "any (default) or all", "any").option(
|
|
3882
4110
|
"--platform <platform>",
|
|
3883
|
-
"Filter matched suites to one platform: ios, android, tvos, androidtv (default: no filter)"
|
|
4111
|
+
"Filter matched suites to one platform: ios, android, tvos, androidtv, roku (default: no filter)"
|
|
3884
4112
|
).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(
|
|
3885
4113
|
"--device-category <category>",
|
|
3886
4114
|
"Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
|
|
@@ -3888,13 +4116,16 @@ program.command("run-suites").description("Run all suites matching a tag filter"
|
|
|
3888
4116
|
).option(
|
|
3889
4117
|
"--device <device_id>",
|
|
3890
4118
|
"Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
|
|
3891
|
-
).option("--build <id>", "AppBuild id (validated against every matched suite)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").
|
|
4119
|
+
).option("--build <id>", "AppBuild id (validated against every matched suite)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
|
|
4120
|
+
"--retry",
|
|
4121
|
+
"Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
|
|
4122
|
+
).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((opts) => {
|
|
3892
4123
|
if (opts.device && opts.deviceCategory) {
|
|
3893
4124
|
process.stderr.write("error: --device cannot be combined with --device-category\n");
|
|
3894
4125
|
process.exit(2);
|
|
3895
4126
|
}
|
|
3896
4127
|
try {
|
|
3897
|
-
|
|
4128
|
+
parseSuitePlatformFilter(opts.platform);
|
|
3898
4129
|
} catch (err) {
|
|
3899
4130
|
process.stderr.write(`error: ${err.message}
|
|
3900
4131
|
`);
|
|
@@ -3917,11 +4148,14 @@ program.command("run-test <test-id>").description("Run a single test case").opti
|
|
|
3917
4148
|
"Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
|
|
3918
4149
|
).option(
|
|
3919
4150
|
"--platform <platform>",
|
|
3920
|
-
"Run the test on another platform (ios, android, tvos, androidtv): runs your app
|
|
4151
|
+
"Run the test on another platform (ios, android, tvos, androidtv): runs your linked app on that platform, else the app with the same bundle id / package"
|
|
3921
4152
|
).option(
|
|
3922
4153
|
"--app <app-id>",
|
|
3923
4154
|
"Run the test against this app instead of its own (e.g. another platform build with a different bundle id)"
|
|
3924
|
-
).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").
|
|
4155
|
+
).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").option(
|
|
4156
|
+
"--retry",
|
|
4157
|
+
"Retry a failed test once, after a fresh start of the app (off by default); a pass on the retry marks the test flaky"
|
|
4158
|
+
).option("--no-retry", "Deprecated: retries are off unless --retry is given (accepted, no effect)").action((testId, opts) => {
|
|
3925
4159
|
if (opts.device && opts.deviceCategory) {
|
|
3926
4160
|
process.stderr.write("error: --device cannot be combined with --device-category\n");
|
|
3927
4161
|
process.exit(2);
|
|
@@ -3969,12 +4203,18 @@ program.command("list-devices").description("List devices registered with the pl
|
|
|
3969
4203
|
program.command("upload-app <file>").description(
|
|
3970
4204
|
"Upload an app build (zipped simulator .app for iOS/tvOS, .apk for Android/Android TV, or a Roku channel .zip)"
|
|
3971
4205
|
).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));
|
|
3972
|
-
program.command("create-test [name]").description("Create
|
|
4206
|
+
program.command("create-test [name]").description("Create an AI test case, optionally in one or more groups").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)").option(
|
|
4207
|
+
"--group <id...>",
|
|
4208
|
+
"Group (suite) ID to add the test to; repeat or list several. Same app as --app"
|
|
4209
|
+
).addOption(
|
|
3973
4210
|
new Option(
|
|
3974
4211
|
"--if-exists <mode>",
|
|
3975
|
-
"When a
|
|
4212
|
+
"When a test with this name already exists for the app (outside any group, or in one of the --group groups): fail, update it, or create a new one alongside it. Default: reuse an identical test, refuse a different one"
|
|
3976
4213
|
).choices(["fail", "update", "new"])
|
|
3977
4214
|
).action((name, opts) => createTest({ ...opts, testName: name || opts.testName || "CLI Test" }));
|
|
4215
|
+
program.command("import-maestro <path>").description(
|
|
4216
|
+
"Import Maestro flows as tests: a flow file, a .zip of your flows folder, or the folder itself"
|
|
4217
|
+
).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("--group <group>", "Put the imported tests in this group (name or id)").option("--dry-run", "Show what would be imported and every flagged command; save nothing", false).action((path, opts) => importMaestro(path, opts));
|
|
3978
4218
|
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));
|
|
3979
4219
|
program.command("logout").description("Revoke the saved login token and remove it from ~/.vibeview/config.json").action(() => logout());
|
|
3980
4220
|
program.command("dev").description(
|
|
@@ -4147,7 +4387,7 @@ program.command("dev-reload").description(
|
|
|
4147
4387
|
program.command("mcp").description(
|
|
4148
4388
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
|
|
4149
4389
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
4150
|
-
const { runMcpServer } = await import("./server-
|
|
4390
|
+
const { runMcpServer } = await import("./server-GEY4WZUH.js");
|
|
4151
4391
|
await runMcpServer({ ...opts, version: pkg.version });
|
|
4152
4392
|
});
|
|
4153
4393
|
program.command("agent-setup").description(
|