@qawolf/cli 1.11.0 → 1.11.2
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/cli.js +206 -79
- package/package.json +2 -2
- package/skills/qawolf-cli/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -8514,7 +8514,7 @@ var require_BufferList = __commonJS((exports, module) => {
|
|
|
8514
8514
|
this.head = this.tail = null;
|
|
8515
8515
|
this.length = 0;
|
|
8516
8516
|
};
|
|
8517
|
-
BufferList.prototype.join = function
|
|
8517
|
+
BufferList.prototype.join = function join36(s3) {
|
|
8518
8518
|
if (this.length === 0)
|
|
8519
8519
|
return "";
|
|
8520
8520
|
var p3 = this.head;
|
|
@@ -23182,7 +23182,7 @@ var require_BufferList2 = __commonJS((exports, module) => {
|
|
|
23182
23182
|
this.head = this.tail = null;
|
|
23183
23183
|
this.length = 0;
|
|
23184
23184
|
};
|
|
23185
|
-
BufferList.prototype.join = function
|
|
23185
|
+
BufferList.prototype.join = function join36(s3) {
|
|
23186
23186
|
if (this.length === 0)
|
|
23187
23187
|
return "";
|
|
23188
23188
|
var p3 = this.head;
|
|
@@ -134457,7 +134457,7 @@ var require_snapshot_utils = __commonJS((exports, module) => {
|
|
|
134457
134457
|
// node_modules/undici/lib/mock/snapshot-recorder.js
|
|
134458
134458
|
var require_snapshot_recorder = __commonJS((exports, module) => {
|
|
134459
134459
|
var { writeFile, readFile, mkdir } = __require("node:fs/promises");
|
|
134460
|
-
var { dirname:
|
|
134460
|
+
var { dirname: dirname12, resolve: resolve10 } = __require("node:path");
|
|
134461
134461
|
var { setTimeout: setTimeout3, clearTimeout: clearTimeout2 } = __require("node:timers");
|
|
134462
134462
|
var { InvalidArgumentError: InvalidArgumentError3, UndiciError } = require_errors7();
|
|
134463
134463
|
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
|
|
@@ -134652,7 +134652,7 @@ var require_snapshot_recorder = __commonJS((exports, module) => {
|
|
|
134652
134652
|
throw new InvalidArgumentError3("Snapshot path is required");
|
|
134653
134653
|
}
|
|
134654
134654
|
const resolvedPath = resolve10(path4);
|
|
134655
|
-
await mkdir(
|
|
134655
|
+
await mkdir(dirname12(resolvedPath), { recursive: true });
|
|
134656
134656
|
const data2 = Array.from(this.#snapshots.entries()).map(([hash2, snapshot]) => ({
|
|
134657
134657
|
hash: hash2,
|
|
134658
134658
|
snapshot
|
|
@@ -156448,6 +156448,12 @@ var interactiveRunnerMessages = {
|
|
|
156448
156448
|
targetMismatch: (runnerName, requiredRunnerName) => `This flow needs a ${requiredRunnerName} runner and ${runnerName} is running. Launch one with --name ${requiredRunnerName}.`
|
|
156449
156449
|
};
|
|
156450
156450
|
// src/core/messages/runner.ts
|
|
156451
|
+
var maxListedNames = 5;
|
|
156452
|
+
function listNames(names) {
|
|
156453
|
+
const shown = names.slice(0, maxListedNames).join(", ");
|
|
156454
|
+
const rest = names.length - maxListedNames;
|
|
156455
|
+
return rest > 0 ? `${shown} and ${rest} more` : shown;
|
|
156456
|
+
}
|
|
156451
156457
|
var runnerMessages = {
|
|
156452
156458
|
playwrightLoadFailed: (envDir, detail) => packageLoadFailed("Playwright", envDir, detail),
|
|
156453
156459
|
androidWorkersUnsupported: "Android flows are not yet supported with --workers > 1; rerun Android flows with --workers 1.",
|
|
@@ -156477,6 +156483,12 @@ ${dir}`,
|
|
|
156477
156483
|
].join(`
|
|
156478
156484
|
`),
|
|
156479
156485
|
outerHopCandidateRejected: (dir, missing) => `outer-hop candidate rejected: ${dir} missing ${missing.join(", ")}`,
|
|
156486
|
+
outerHopFallbackNotice: (dir, missing) => [
|
|
156487
|
+
`Not reusing ${dir} — it is missing ${listNames(missing)}.`,
|
|
156488
|
+
`Installing the declared dependencies instead. Run 'npm install' there to reuse it.`
|
|
156489
|
+
].join(`
|
|
156490
|
+
`),
|
|
156491
|
+
outerHopCarriedOver: (names) => `Carried over ${pluralize(names.length, "undeclared package")} from the project node_modules: ${listNames(names)}. Declare them in package.json "dependencies".`,
|
|
156480
156492
|
moduleNotFoundHint: (pkg, projectDir) => projectDir === undefined ? `Hint: '${pkg}' could not be resolved. Run from within your flows project so its dependencies can be found.` : `Hint: '${pkg}' could not be resolved. Ensure it is declared in ${projectDir}/package.json "dependencies" and run npm install in that project.`
|
|
156481
156493
|
};
|
|
156482
156494
|
// src/core/messages/updateCheck.ts
|
|
@@ -175911,7 +175923,7 @@ function startUpdateCheck(deps) {
|
|
|
175911
175923
|
// package.json
|
|
175912
175924
|
var package_default = {
|
|
175913
175925
|
name: "@qawolf/cli",
|
|
175914
|
-
version: "1.11.
|
|
175926
|
+
version: "1.11.2",
|
|
175915
175927
|
description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
175916
175928
|
keywords: [
|
|
175917
175929
|
"automation",
|
|
@@ -175979,7 +175991,6 @@ var package_default = {
|
|
|
175979
175991
|
appium: "2.11.3",
|
|
175980
175992
|
commander: "14.0.3",
|
|
175981
175993
|
"env-paths": "4.0.0",
|
|
175982
|
-
"expect-webdriverio": "5.6.5",
|
|
175983
175994
|
picomatch: "4.0.4",
|
|
175984
175995
|
pino: "10.3.1",
|
|
175985
175996
|
playwright: "1.62.0",
|
|
@@ -175996,6 +176007,7 @@ var package_default = {
|
|
|
175996
176007
|
"@types/bun": "1.3.14",
|
|
175997
176008
|
"@types/picomatch": "4.0.3",
|
|
175998
176009
|
"appium-uiautomator2-driver": "4.2.9",
|
|
176010
|
+
"expect-webdriverio": "5.6.5",
|
|
175999
176011
|
knip: "6.16.1",
|
|
176000
176012
|
oxfmt: "0.54.0",
|
|
176001
176013
|
oxlint: "1.69.0",
|
|
@@ -177789,7 +177801,8 @@ async function expandPatterns(patterns, cwd, logger, fs3) {
|
|
|
177789
177801
|
for (const root of roots) {
|
|
177790
177802
|
const matches = await glob(effectivePatterns, {
|
|
177791
177803
|
cwd: root,
|
|
177792
|
-
absolute: true
|
|
177804
|
+
absolute: true,
|
|
177805
|
+
ignore: ["**/node_modules/**"]
|
|
177793
177806
|
});
|
|
177794
177807
|
for (const match of matches) {
|
|
177795
177808
|
const file2 = resolve3(match);
|
|
@@ -182468,6 +182481,14 @@ function buildPatternArgs(pattern) {
|
|
|
182468
182481
|
return pattern ? [pattern] : [];
|
|
182469
182482
|
}
|
|
182470
182483
|
|
|
182484
|
+
// src/core/flowFailureHint.ts
|
|
182485
|
+
function flowFailureHint(errText, projectDir) {
|
|
182486
|
+
const missingPackage = extractMissingPackage(errText);
|
|
182487
|
+
if (missingPackage === undefined)
|
|
182488
|
+
return;
|
|
182489
|
+
return runnerMessages.moduleNotFoundHint(missingPackage, projectDir);
|
|
182490
|
+
}
|
|
182491
|
+
|
|
182471
182492
|
// src/shell/reporter/createCompositeReporter.ts
|
|
182472
182493
|
function callEach(reporters, pick2, event) {
|
|
182473
182494
|
for (const r2 of reporters) {
|
|
@@ -182534,6 +182555,26 @@ function formatErrorWithCause(err) {
|
|
|
182534
182555
|
`);
|
|
182535
182556
|
}
|
|
182536
182557
|
|
|
182558
|
+
// src/domains/runner/jsonFailureDetails.ts
|
|
182559
|
+
function collectJsonFailureDetails(reporter, projectDir) {
|
|
182560
|
+
const details = [];
|
|
182561
|
+
return {
|
|
182562
|
+
reporter: createCompositeReporter([
|
|
182563
|
+
reporter,
|
|
182564
|
+
{
|
|
182565
|
+
onFlowFail: ({ err }) => {
|
|
182566
|
+
const detail = formatErrorWithCause(err);
|
|
182567
|
+
const hint = flowFailureHint(detail, projectDir);
|
|
182568
|
+
details.push(hint === undefined ? detail : `${detail}
|
|
182569
|
+
|
|
182570
|
+
${hint}`);
|
|
182571
|
+
}
|
|
182572
|
+
}
|
|
182573
|
+
]),
|
|
182574
|
+
details
|
|
182575
|
+
};
|
|
182576
|
+
}
|
|
182577
|
+
|
|
182537
182578
|
// src/domains/runner/runAndroidFlowUtils.ts
|
|
182538
182579
|
import { parseExecutionTarget as parseExecutionTarget3 } from "@qawolf/flow-targets";
|
|
182539
182580
|
|
|
@@ -182589,7 +182630,7 @@ class FlowRunError extends Error {
|
|
|
182589
182630
|
}
|
|
182590
182631
|
}
|
|
182591
182632
|
|
|
182592
|
-
// src/domains/runner/
|
|
182633
|
+
// src/domains/runner/dispatchFlow.ts
|
|
182593
182634
|
async function dispatchFlow({
|
|
182594
182635
|
deps,
|
|
182595
182636
|
flow,
|
|
@@ -182881,13 +182922,9 @@ async function flowsRun(ctx, files, flags, deps) {
|
|
|
182881
182922
|
return { error: runnerMessages.androidWorkersUnsupported, exitCode: 2 };
|
|
182882
182923
|
}
|
|
182883
182924
|
const { webOptions, androidOptions } = buildRunOptions(flags);
|
|
182884
|
-
const
|
|
182885
|
-
const
|
|
182886
|
-
|
|
182887
|
-
{
|
|
182888
|
-
onFlowFail: ({ err }) => failureDetails.push(formatErrorWithCause(err))
|
|
182889
|
-
}
|
|
182890
|
-
]) : deps.reporter;
|
|
182925
|
+
const collected = ctx.outputMode === "json" ? collectJsonFailureDetails(deps.reporter, deps.projectDir) : undefined;
|
|
182926
|
+
const failureDetails = collected?.details ?? [];
|
|
182927
|
+
const reporter = collected?.reporter ?? deps.reporter;
|
|
182891
182928
|
const result = await executeFlows({
|
|
182892
182929
|
ctx,
|
|
182893
182930
|
deps: { ...deps, reporter },
|
|
@@ -182975,7 +183012,7 @@ async function defaultRunWebFlowDeps(envDir = process.cwd(), signals) {
|
|
|
182975
183012
|
|
|
182976
183013
|
// src/domains/runtimeEnv/prepareRunDir.ts
|
|
182977
183014
|
import { createHash as createHash4 } from "node:crypto";
|
|
182978
|
-
import { join as
|
|
183015
|
+
import { join as join33, resolve as resolve9 } from "node:path";
|
|
182979
183016
|
|
|
182980
183017
|
// src/domains/runtimeEnv/execSubpathImports.ts
|
|
182981
183018
|
import { join as join26 } from "node:path";
|
|
@@ -183044,9 +183081,82 @@ async function populateInnerHop(args) {
|
|
|
183044
183081
|
}
|
|
183045
183082
|
|
|
183046
183083
|
// src/domains/runtimeEnv/outerHop.ts
|
|
183084
|
+
import { lstat as lstat2 } from "node:fs/promises";
|
|
183085
|
+
import { dirname as dirname10, join as join30 } from "node:path";
|
|
183086
|
+
|
|
183087
|
+
// src/domains/runtimeEnv/carryOverPackages.ts
|
|
183047
183088
|
import { lstat } from "node:fs/promises";
|
|
183048
183089
|
import { dirname as dirname9, join as join29 } from "node:path";
|
|
183090
|
+
|
|
183091
|
+
// src/domains/runtimeEnv/carryOverPlan.ts
|
|
183049
183092
|
var executorPackageNames = new Set(pinnedPackages.map((p3) => p3.name));
|
|
183093
|
+
function planCarryOver(args) {
|
|
183094
|
+
const installed = new Set(args.installed);
|
|
183095
|
+
return args.present.filter((name) => !name.startsWith(".") && !installed.has(name) && !executorPackageNames.has(name));
|
|
183096
|
+
}
|
|
183097
|
+
|
|
183098
|
+
// src/domains/runtimeEnv/carryOverPackages.ts
|
|
183099
|
+
async function carryOverPackages(args) {
|
|
183100
|
+
const { projectModulesDir, nodeModulesDir, fs: fs4 } = args;
|
|
183101
|
+
const present = await listPackageNames(projectModulesDir, fs4);
|
|
183102
|
+
if (present.length === 0)
|
|
183103
|
+
return [];
|
|
183104
|
+
const names = planCarryOver({
|
|
183105
|
+
present,
|
|
183106
|
+
installed: await listPackageNames(nodeModulesDir, fs4)
|
|
183107
|
+
});
|
|
183108
|
+
const linked = [];
|
|
183109
|
+
for (const name of names) {
|
|
183110
|
+
const segments = name.split("/");
|
|
183111
|
+
const target = join29(nodeModulesDir, ...segments);
|
|
183112
|
+
if (await pathOccupied(target))
|
|
183113
|
+
continue;
|
|
183114
|
+
if (segments.length > 1)
|
|
183115
|
+
await fs4.mkdir(dirname9(target), { recursive: true });
|
|
183116
|
+
await createDirSymlink(join29(projectModulesDir, ...segments), target);
|
|
183117
|
+
linked.push(name);
|
|
183118
|
+
}
|
|
183119
|
+
return linked;
|
|
183120
|
+
}
|
|
183121
|
+
async function pathOccupied(path3) {
|
|
183122
|
+
try {
|
|
183123
|
+
await lstat(path3);
|
|
183124
|
+
return true;
|
|
183125
|
+
} catch {
|
|
183126
|
+
return false;
|
|
183127
|
+
}
|
|
183128
|
+
}
|
|
183129
|
+
async function listPackageNames(dir, fs4) {
|
|
183130
|
+
let entries;
|
|
183131
|
+
try {
|
|
183132
|
+
entries = await fs4.readdir(dir);
|
|
183133
|
+
} catch {
|
|
183134
|
+
return [];
|
|
183135
|
+
}
|
|
183136
|
+
const names = [];
|
|
183137
|
+
for (const entry of entries) {
|
|
183138
|
+
if (!entry.startsWith("@")) {
|
|
183139
|
+
if (fs4.existsSync(join29(dir, entry, "package.json")))
|
|
183140
|
+
names.push(entry);
|
|
183141
|
+
continue;
|
|
183142
|
+
}
|
|
183143
|
+
let scoped;
|
|
183144
|
+
try {
|
|
183145
|
+
scoped = await fs4.readdir(join29(dir, entry));
|
|
183146
|
+
} catch {
|
|
183147
|
+
continue;
|
|
183148
|
+
}
|
|
183149
|
+
for (const inner of scoped) {
|
|
183150
|
+
if (fs4.existsSync(join29(dir, entry, inner, "package.json"))) {
|
|
183151
|
+
names.push(`${entry}/${inner}`);
|
|
183152
|
+
}
|
|
183153
|
+
}
|
|
183154
|
+
}
|
|
183155
|
+
return names;
|
|
183156
|
+
}
|
|
183157
|
+
|
|
183158
|
+
// src/domains/runtimeEnv/outerHop.ts
|
|
183159
|
+
var executorPackageNames2 = new Set(pinnedPackages.map((p3) => p3.name));
|
|
183050
183160
|
async function populateOuterHop(args) {
|
|
183051
183161
|
const { projectDir, runDir, fs: fs4 } = args;
|
|
183052
183162
|
if (projectDir === undefined)
|
|
@@ -183055,7 +183165,7 @@ async function populateOuterHop(args) {
|
|
|
183055
183165
|
const depNames = Object.keys(installableDeps);
|
|
183056
183166
|
const { satisfying, rejected } = await findSatisfyingNodeModulesDir(projectDir, depNames, fs4);
|
|
183057
183167
|
if (satisfying !== undefined) {
|
|
183058
|
-
await createDirSymlink(satisfying,
|
|
183168
|
+
await createDirSymlink(satisfying, join30(runDir, "node_modules"));
|
|
183059
183169
|
return { mode: "symlink", nodeModulesDir: satisfying };
|
|
183060
183170
|
}
|
|
183061
183171
|
if (depNames.length === 0)
|
|
@@ -183064,24 +183174,29 @@ async function populateOuterHop(args) {
|
|
|
183064
183174
|
await installOuterDeps(runDir, installableDeps, fs4, args.install ?? spawnNpmInstall);
|
|
183065
183175
|
await linkPinnedPackages({
|
|
183066
183176
|
depsRoot: args.depsRoot,
|
|
183067
|
-
nodeModulesDir:
|
|
183177
|
+
nodeModulesDir: join30(runDir, "node_modules"),
|
|
183068
183178
|
fs: fs4
|
|
183069
183179
|
});
|
|
183070
|
-
|
|
183180
|
+
const carriedOver = await carryOverPackages({
|
|
183181
|
+
projectModulesDir: join30(projectDir, "node_modules"),
|
|
183182
|
+
nodeModulesDir: join30(runDir, "node_modules"),
|
|
183183
|
+
fs: fs4
|
|
183184
|
+
});
|
|
183185
|
+
return { mode: "install", depCount: depNames.length, rejected, carriedOver };
|
|
183071
183186
|
}
|
|
183072
183187
|
async function findSatisfyingNodeModulesDir(startDir, depNames, fs4) {
|
|
183073
183188
|
const rejected = [];
|
|
183074
183189
|
let dir = startDir;
|
|
183075
183190
|
while (true) {
|
|
183076
|
-
const candidate =
|
|
183191
|
+
const candidate = join30(dir, "node_modules");
|
|
183077
183192
|
const stats = await lstatSafe(candidate);
|
|
183078
183193
|
if (stats !== undefined && stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
183079
|
-
const missing = depNames.filter((name) => !fs4.existsSync(
|
|
183194
|
+
const missing = depNames.filter((name) => !fs4.existsSync(join30(candidate, name, "package.json")));
|
|
183080
183195
|
if (missing.length === 0)
|
|
183081
183196
|
return { satisfying: candidate, rejected };
|
|
183082
183197
|
rejected.push({ dir: candidate, missing });
|
|
183083
183198
|
}
|
|
183084
|
-
const parent =
|
|
183199
|
+
const parent = dirname10(dir);
|
|
183085
183200
|
if (parent === dir)
|
|
183086
183201
|
return { satisfying: undefined, rejected };
|
|
183087
183202
|
dir = parent;
|
|
@@ -183089,7 +183204,7 @@ async function findSatisfyingNodeModulesDir(startDir, depNames, fs4) {
|
|
|
183089
183204
|
}
|
|
183090
183205
|
async function lstatSafe(path3) {
|
|
183091
183206
|
try {
|
|
183092
|
-
return await
|
|
183207
|
+
return await lstat2(path3);
|
|
183093
183208
|
} catch {
|
|
183094
183209
|
return;
|
|
183095
183210
|
}
|
|
@@ -183097,7 +183212,7 @@ async function lstatSafe(path3) {
|
|
|
183097
183212
|
async function readInstallableDeps(projectDir, fs4) {
|
|
183098
183213
|
let content;
|
|
183099
183214
|
try {
|
|
183100
|
-
content = await fs4.readFile(
|
|
183215
|
+
content = await fs4.readFile(join30(projectDir, "package.json"));
|
|
183101
183216
|
} catch {
|
|
183102
183217
|
return {};
|
|
183103
183218
|
}
|
|
@@ -183112,11 +183227,11 @@ async function readInstallableDeps(projectDir, fs4) {
|
|
|
183112
183227
|
const rawDeps = parsed["dependencies"];
|
|
183113
183228
|
if (typeof rawDeps !== "object" || rawDeps === null)
|
|
183114
183229
|
return {};
|
|
183115
|
-
return Object.fromEntries(Object.entries(rawDeps).filter(([name]) => !
|
|
183230
|
+
return Object.fromEntries(Object.entries(rawDeps).filter(([name]) => !executorPackageNames2.has(name)).filter((entry) => typeof entry[1] === "string"));
|
|
183116
183231
|
}
|
|
183117
183232
|
async function installOuterDeps(runDir, deps, fs4, install) {
|
|
183118
|
-
await fs4.writeFile(
|
|
183119
|
-
await fs4.writeFile(
|
|
183233
|
+
await fs4.writeFile(join30(runDir, "package.json"), JSON.stringify({ name: "qawolf-run", private: true, dependencies: deps }, undefined, 2));
|
|
183234
|
+
await fs4.writeFile(join30(runDir, ".npmrc"), `@qawolf:registry=https://registry.npmjs.org/
|
|
183120
183235
|
`);
|
|
183121
183236
|
const result = await install(runDir);
|
|
183122
183237
|
if (result.exitCode !== 0) {
|
|
@@ -183126,14 +183241,14 @@ async function installOuterDeps(runDir, deps, fs4, install) {
|
|
|
183126
183241
|
|
|
183127
183242
|
// src/domains/runtimeEnv/stageFlowFiles.ts
|
|
183128
183243
|
import { createHash as createHash3 } from "node:crypto";
|
|
183129
|
-
import { basename as basename4, dirname as
|
|
183244
|
+
import { basename as basename4, dirname as dirname11, join as join32, sep as sep4 } from "node:path";
|
|
183130
183245
|
|
|
183131
183246
|
// src/shell/copyDir.ts
|
|
183132
183247
|
import { cp, readdir as readdir2 } from "node:fs/promises";
|
|
183133
|
-
import { basename as basename3, join as
|
|
183248
|
+
import { basename as basename3, join as join31 } from "node:path";
|
|
183134
183249
|
async function copyDirExcluding(source, destination, excludedNames) {
|
|
183135
183250
|
const entries = await readdir2(source, { withFileTypes: true });
|
|
183136
|
-
await Promise.all(entries.filter((entry) => !excludedNames.has(entry.name)).map((entry) => cp(
|
|
183251
|
+
await Promise.all(entries.filter((entry) => !excludedNames.has(entry.name)).map((entry) => cp(join31(source, entry.name), join31(destination, entry.name), {
|
|
183137
183252
|
recursive: true,
|
|
183138
183253
|
filter: (path3) => !excludedNames.has(basename3(path3))
|
|
183139
183254
|
})));
|
|
@@ -183150,14 +183265,14 @@ async function stageFlowFiles(args) {
|
|
|
183150
183265
|
if (files.length > 1) {
|
|
183151
183266
|
return Promise.all(files.map((f3) => stageOneFile(f3, execDir, fs4)));
|
|
183152
183267
|
}
|
|
183153
|
-
await Promise.all(files.map((f3) => fs4.copyFile(f3,
|
|
183154
|
-
return files.map((f3) =>
|
|
183268
|
+
await Promise.all(files.map((f3) => fs4.copyFile(f3, join32(execDir, basename4(f3)))));
|
|
183269
|
+
return files.map((f3) => join32(execDir, basename4(f3)));
|
|
183155
183270
|
}
|
|
183156
183271
|
async function stageOneFile(file2, execDir, fs4) {
|
|
183157
|
-
const dirHash = createHash3("sha256").update(
|
|
183158
|
-
const subDir =
|
|
183272
|
+
const dirHash = createHash3("sha256").update(dirname11(file2)).digest("hex").slice(0, 8);
|
|
183273
|
+
const subDir = join32(execDir, dirHash);
|
|
183159
183274
|
await fs4.mkdir(subDir, { recursive: true });
|
|
183160
|
-
const dest =
|
|
183275
|
+
const dest = join32(subDir, basename4(file2));
|
|
183161
183276
|
await fs4.copyFile(file2, dest);
|
|
183162
183277
|
return dest;
|
|
183163
183278
|
}
|
|
@@ -183165,7 +183280,7 @@ function remapPath(file2, projectDir, execDir) {
|
|
|
183165
183280
|
if (file2 === projectDir)
|
|
183166
183281
|
return execDir;
|
|
183167
183282
|
if (file2.startsWith(projectDir + sep4)) {
|
|
183168
|
-
return
|
|
183283
|
+
return join32(execDir, file2.slice(projectDir.length + 1));
|
|
183169
183284
|
}
|
|
183170
183285
|
return;
|
|
183171
183286
|
}
|
|
@@ -183175,10 +183290,10 @@ async function prepareRunDir(args) {
|
|
|
183175
183290
|
const { files, projectDir, depsRoot, runRoot } = args;
|
|
183176
183291
|
const fs4 = args.fs ?? makeDefaultFs();
|
|
183177
183292
|
const runId = buildRunId(projectDir, files);
|
|
183178
|
-
const runDir =
|
|
183293
|
+
const runDir = join33(runRoot, runId);
|
|
183179
183294
|
await fs4.rm(runDir, { recursive: true, force: true });
|
|
183180
183295
|
await fs4.mkdir(runDir, { recursive: true });
|
|
183181
|
-
const execDir =
|
|
183296
|
+
const execDir = join33(runDir, "exec");
|
|
183182
183297
|
await fs4.mkdir(execDir, { recursive: true });
|
|
183183
183298
|
await populateInnerHop({ depsRoot, execDir, fs: fs4 });
|
|
183184
183299
|
const stagedFiles = await stageFlowFiles({ files, projectDir, execDir, fs: fs4 });
|
|
@@ -183209,17 +183324,17 @@ function buildRunId(projectDir, files) {
|
|
|
183209
183324
|
}
|
|
183210
183325
|
|
|
183211
183326
|
// src/shell/testkit.ts
|
|
183212
|
-
import { join as
|
|
183327
|
+
import { join as join34 } from "node:path";
|
|
183213
183328
|
function notAvailableLocally(name) {
|
|
183214
183329
|
throw new Error(runnerMessages.notAvailableLocally(name));
|
|
183215
183330
|
}
|
|
183216
183331
|
async function loadSdkDeps(cwd) {
|
|
183217
|
-
const testkitDir =
|
|
183332
|
+
const testkitDir = join34(cwd, "node_modules", "@qawolf", "testkit");
|
|
183218
183333
|
try {
|
|
183219
183334
|
const clientMod = await importFromPath(resolveFromEnvDir(cwd, "@qawolf/testkit/client"));
|
|
183220
183335
|
if (typeof clientMod.createTestkitClient !== "function")
|
|
183221
183336
|
throw new Error("createTestkitClient is not a function");
|
|
183222
|
-
const scopeMod = await importFromPath(
|
|
183337
|
+
const scopeMod = await importFromPath(join34(testkitDir, "dist", "clientScope.js"));
|
|
183223
183338
|
if (typeof scopeMod.configureTestkitClient !== "function")
|
|
183224
183339
|
throw new Error("configureTestkitClient is not a function");
|
|
183225
183340
|
return {
|
|
@@ -183277,9 +183392,9 @@ async function registerLazyEmailsClient(getInbox, cwd, deps) {
|
|
|
183277
183392
|
}
|
|
183278
183393
|
|
|
183279
183394
|
// src/commands/flows/envVarDeps.ts
|
|
183280
|
-
import { join as
|
|
183395
|
+
import { join as join35 } from "node:path";
|
|
183281
183396
|
function envFilePath(envDir) {
|
|
183282
|
-
return
|
|
183397
|
+
return join35(envDir, ".env");
|
|
183283
183398
|
}
|
|
183284
183399
|
async function readEnv(envDir, fs4) {
|
|
183285
183400
|
try {
|
|
@@ -183383,6 +183498,9 @@ async function createFlowRuntimeDeps({
|
|
|
183383
183498
|
};
|
|
183384
183499
|
}
|
|
183385
183500
|
|
|
183501
|
+
// src/commands/flows/runStagedFlows.ts
|
|
183502
|
+
import { join as join39 } from "node:path";
|
|
183503
|
+
|
|
183386
183504
|
// src/shell/appium/createAppiumServer.ts
|
|
183387
183505
|
import { existsSync as existsSync2 } from "node:fs";
|
|
183388
183506
|
|
|
@@ -183746,12 +183864,12 @@ Install Android Studio and open Tools > SDK Manager.`);
|
|
|
183746
183864
|
}
|
|
183747
183865
|
|
|
183748
183866
|
// src/shell/manifest/lookup.ts
|
|
183749
|
-
import { basename as basename5, dirname as
|
|
183867
|
+
import { basename as basename5, dirname as dirname12, relative as relative6 } from "node:path";
|
|
183750
183868
|
async function findFlowStamp(flowAbsPath) {
|
|
183751
183869
|
let envDir;
|
|
183752
|
-
let current =
|
|
183753
|
-
while (current !==
|
|
183754
|
-
const parent2 =
|
|
183870
|
+
let current = dirname12(flowAbsPath);
|
|
183871
|
+
while (current !== dirname12(current)) {
|
|
183872
|
+
const parent2 = dirname12(current);
|
|
183755
183873
|
if (basename5(parent2) === ".qawolf") {
|
|
183756
183874
|
envDir = current;
|
|
183757
183875
|
break;
|
|
@@ -184683,14 +184801,14 @@ async function runWebFlow({
|
|
|
184683
184801
|
|
|
184684
184802
|
// src/shell/embeddedWorkerCli.ts
|
|
184685
184803
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
184686
|
-
import { basename as basename6, join as
|
|
184804
|
+
import { basename as basename6, join as join36 } from "node:path";
|
|
184687
184805
|
var embeddedCliPathEnv = "QAWOLF_EMBEDDED_CLI_PATH";
|
|
184688
184806
|
function extractEmbeddedWorkerCli() {
|
|
184689
184807
|
const assetPath = process.env[embeddedCliPathEnv];
|
|
184690
184808
|
if (assetPath === undefined || assetPath === "")
|
|
184691
184809
|
return;
|
|
184692
|
-
const dir =
|
|
184693
|
-
const dest =
|
|
184810
|
+
const dir = join36(getDataDir(), "worker");
|
|
184811
|
+
const dest = join36(dir, basename6(assetPath));
|
|
184694
184812
|
if (existsSync3(dest))
|
|
184695
184813
|
return dest;
|
|
184696
184814
|
mkdirSync3(dir, { recursive: true });
|
|
@@ -184845,7 +184963,7 @@ function makePooledDispatch(resolvedDir) {
|
|
|
184845
184963
|
}
|
|
184846
184964
|
|
|
184847
184965
|
// src/commands/flows/buildRunReporter.ts
|
|
184848
|
-
import { dirname as
|
|
184966
|
+
import { dirname as dirname13 } from "node:path";
|
|
184849
184967
|
|
|
184850
184968
|
// src/shell/reporter/createConsoleReporter.ts
|
|
184851
184969
|
import { styleText as styleText3 } from "node:util";
|
|
@@ -184892,9 +185010,9 @@ function createConsoleReporter(deps) {
|
|
|
184892
185010
|
deps.stderr.write(`${formatted}
|
|
184893
185011
|
|
|
184894
185012
|
`);
|
|
184895
|
-
const
|
|
184896
|
-
if (
|
|
184897
|
-
deps.stderr.write(`${styleText3("yellow", `${indent}${
|
|
185013
|
+
const hint = flowFailureHint(errStr, deps.projectDir);
|
|
185014
|
+
if (hint !== undefined) {
|
|
185015
|
+
deps.stderr.write(`${styleText3("yellow", `${indent}${hint}`)}
|
|
184898
185016
|
|
|
184899
185017
|
`);
|
|
184900
185018
|
}
|
|
@@ -184928,7 +185046,7 @@ ${styleText3("dim", "─".repeat(width))}
|
|
|
184928
185046
|
};
|
|
184929
185047
|
}
|
|
184930
185048
|
// src/shell/reporter/createJUnitReporter.ts
|
|
184931
|
-
import { join as
|
|
185049
|
+
import { join as join37 } from "node:path";
|
|
184932
185050
|
|
|
184933
185051
|
// src/shell/reporter/junitXml.ts
|
|
184934
185052
|
function escapeXml(str) {
|
|
@@ -184967,7 +185085,7 @@ ${suites}`;
|
|
|
184967
185085
|
|
|
184968
185086
|
// src/shell/reporter/createJUnitReporter.ts
|
|
184969
185087
|
function resolveJUnitOutputPath(junit, outputDir) {
|
|
184970
|
-
return typeof junit === "string" && junit.trim() !== "" ? junit :
|
|
185088
|
+
return typeof junit === "string" && junit.trim() !== "" ? junit : join37(outputDir, "junit-report.xml");
|
|
184971
185089
|
}
|
|
184972
185090
|
function createJUnitReporter(deps) {
|
|
184973
185091
|
const flows = [];
|
|
@@ -185006,7 +185124,7 @@ function buildRunReporter(flags, deps) {
|
|
|
185006
185124
|
const junit = createJUnitReporter({
|
|
185007
185125
|
outputPath: resolveJUnitOutputPath(flags.junit, flags.outputDir),
|
|
185008
185126
|
writeFile: (path9, content) => {
|
|
185009
|
-
deps.fs.mkdirSync(
|
|
185127
|
+
deps.fs.mkdirSync(dirname13(path9), { recursive: true });
|
|
185010
185128
|
deps.fs.writeFileSync(path9, content);
|
|
185011
185129
|
},
|
|
185012
185130
|
stderr
|
|
@@ -185045,6 +185163,7 @@ function buildFlowsRunDeps(args) {
|
|
|
185045
185163
|
findFlowStamp,
|
|
185046
185164
|
warn: (message) => ctx.ui.warn(message),
|
|
185047
185165
|
logger: ctx.log("runner"),
|
|
185166
|
+
...projectDir !== undefined ? { projectDir } : {},
|
|
185048
185167
|
reporter: buildRunReporter(flags, {
|
|
185049
185168
|
fs: ctx.fs,
|
|
185050
185169
|
stdout: { write: (text3) => ctx.ui.write(text3) },
|
|
@@ -185057,11 +185176,11 @@ function buildFlowsRunDeps(args) {
|
|
|
185057
185176
|
|
|
185058
185177
|
// src/commands/flows/loadEnvFile.ts
|
|
185059
185178
|
import { readFile } from "node:fs/promises";
|
|
185060
|
-
import { join as
|
|
185179
|
+
import { join as join38 } from "node:path";
|
|
185061
185180
|
async function loadEnvFile(envDir) {
|
|
185062
185181
|
let content;
|
|
185063
185182
|
try {
|
|
185064
|
-
content = await readFile(
|
|
185183
|
+
content = await readFile(join38(envDir, ".env"), "utf8");
|
|
185065
185184
|
} catch (err) {
|
|
185066
185185
|
if (isNoEntError(err))
|
|
185067
185186
|
return;
|
|
@@ -185106,6 +185225,14 @@ async function runStagedFlows(args) {
|
|
|
185106
185225
|
for (const candidate of staged.outerHop.rejected) {
|
|
185107
185226
|
log31.debug(runnerMessages.outerHopCandidateRejected(candidate.dir, candidate.missing));
|
|
185108
185227
|
}
|
|
185228
|
+
const ownModules = projectDir === undefined ? undefined : join39(projectDir, "node_modules");
|
|
185229
|
+
const own = staged.outerHop.rejected.find((c3) => c3.dir === ownModules);
|
|
185230
|
+
if (own !== undefined) {
|
|
185231
|
+
ctx.ui.warn(runnerMessages.outerHopFallbackNotice(own.dir, own.missing));
|
|
185232
|
+
}
|
|
185233
|
+
if (staged.outerHop.carriedOver.length > 0) {
|
|
185234
|
+
ctx.ui.warn(runnerMessages.outerHopCarriedOver(staged.outerHop.carriedOver));
|
|
185235
|
+
}
|
|
185109
185236
|
}
|
|
185110
185237
|
const unregisterCleanup = ctx.signals.register(staged.cleanup);
|
|
185111
185238
|
try {
|
|
@@ -185162,7 +185289,7 @@ async function handleFlowsRun(ctx, pattern, flags, deps) {
|
|
|
185162
185289
|
}
|
|
185163
185290
|
|
|
185164
185291
|
// src/commands/flows/hybridRunDefaults.ts
|
|
185165
|
-
import { join as
|
|
185292
|
+
import { join as join40, resolve as resolve10 } from "node:path";
|
|
185166
185293
|
function makeDefaultHybridDeps(fs9) {
|
|
185167
185294
|
return {
|
|
185168
185295
|
expandPatterns: (patterns, cwd, logger) => expandPatterns(patterns, cwd, logger, fs9),
|
|
@@ -185181,7 +185308,7 @@ async function handleHybridFlowsRun(ctx, pattern, flags, deps) {
|
|
|
185181
185308
|
if (validation !== "ok") {
|
|
185182
185309
|
return { error: validation.error, exitCode: 2 };
|
|
185183
185310
|
}
|
|
185184
|
-
const envDir = resolve10(
|
|
185311
|
+
const envDir = resolve10(join40(".qawolf", flags.env));
|
|
185185
185312
|
const patternArgs = buildPatternArgs(pattern);
|
|
185186
185313
|
const globFlows = () => resolvedDeps.expandPatterns(patternArgs, envDir, ctx.log("flows"));
|
|
185187
185314
|
let files = await globFlows();
|
|
@@ -185327,12 +185454,12 @@ function registerFlowsCommand(program2, signals) {
|
|
|
185327
185454
|
}
|
|
185328
185455
|
|
|
185329
185456
|
// src/domains/init/init.ts
|
|
185330
|
-
import { dirname as
|
|
185457
|
+
import { dirname as dirname14, join as join42, relative as relative7 } from "node:path";
|
|
185331
185458
|
|
|
185332
185459
|
// src/domains/init/ensurePackageJson.ts
|
|
185333
|
-
import { join as
|
|
185460
|
+
import { join as join41 } from "node:path";
|
|
185334
185461
|
async function ensurePackageJson(ctx, yes, deps) {
|
|
185335
|
-
const pkgPath =
|
|
185462
|
+
const pkgPath = join41(deps.cwd, "package.json");
|
|
185336
185463
|
if (!await deps.fs.pathExists(pkgPath)) {
|
|
185337
185464
|
const confirmed2 = await ctx.ui.confirm(initMessages.createPackageJsonPrompt, { yes });
|
|
185338
185465
|
if (!confirmed2.ok || !confirmed2.value) {
|
|
@@ -185458,9 +185585,9 @@ function makeDefaultInitDeps() {
|
|
|
185458
185585
|
async function handleInit(ctx, opts, deps = makeDefaultInitDeps()) {
|
|
185459
185586
|
ctx.ui.gap();
|
|
185460
185587
|
ctx.ui.intro(initMessages.title);
|
|
185461
|
-
const configPath =
|
|
185462
|
-
const flowPath =
|
|
185463
|
-
const gitignorePath =
|
|
185588
|
+
const configPath = join42(deps.cwd, "qawolf.config.ts");
|
|
185589
|
+
const flowPath = join42(deps.cwd, "src", "flows", "example.flow.ts");
|
|
185590
|
+
const gitignorePath = join42(deps.cwd, ".qawolf", ".gitignore");
|
|
185464
185591
|
await writeWithPrompt(ctx, configPath, qawolfConfigTs, opts.yes, deps);
|
|
185465
185592
|
await writeWithPrompt(ctx, flowPath, exampleFlowTs, opts.yes, deps);
|
|
185466
185593
|
await writeWithPrompt(ctx, gitignorePath, qawolfGitignore, opts.yes, deps);
|
|
@@ -185479,7 +185606,7 @@ async function writeWithPrompt(ctx, filePath, content, yes, deps) {
|
|
|
185479
185606
|
return;
|
|
185480
185607
|
}
|
|
185481
185608
|
}
|
|
185482
|
-
await deps.fs.mkdir(
|
|
185609
|
+
await deps.fs.mkdir(dirname14(filePath), { recursive: true });
|
|
185483
185610
|
await deps.fs.writeFile(filePath, content);
|
|
185484
185611
|
ctx.ui.step(initMessages.createdFile(relPath));
|
|
185485
185612
|
}
|
|
@@ -185498,7 +185625,7 @@ Examples:
|
|
|
185498
185625
|
import { parseExecutionTarget as parseExecutionTarget4 } from "@qawolf/flow-targets";
|
|
185499
185626
|
|
|
185500
185627
|
// src/domains/install/android/avd.ts
|
|
185501
|
-
import { join as
|
|
185628
|
+
import { join as join43 } from "node:path";
|
|
185502
185629
|
async function installAvds(ctx, specs, deps) {
|
|
185503
185630
|
const sdkManagerPath = sdkManagerBin(deps.androidHome, deps.platform);
|
|
185504
185631
|
const avdManagerPath = avdManagerBin(deps.androidHome, deps.platform);
|
|
@@ -185508,7 +185635,7 @@ async function installAvds(ctx, specs, deps) {
|
|
|
185508
185635
|
if (versionResult.exitCode !== 0) {
|
|
185509
185636
|
throw new Error(installMessages.android.sdkmanagerNotFound(sdkManagerPath));
|
|
185510
185637
|
}
|
|
185511
|
-
const licenseFile =
|
|
185638
|
+
const licenseFile = join43(deps.androidHome, "licenses", "android-sdk-license");
|
|
185512
185639
|
if (deps.checkExists(licenseFile)) {
|
|
185513
185640
|
ctx.ui.info(installMessages.android.licensesAlreadyAccepted);
|
|
185514
185641
|
} else {
|
|
@@ -185523,7 +185650,7 @@ async function installAvds(ctx, specs, deps) {
|
|
|
185523
185650
|
for (const spec of specs) {
|
|
185524
185651
|
if (installedImages.has(spec.systemImage))
|
|
185525
185652
|
continue;
|
|
185526
|
-
const systemImg =
|
|
185653
|
+
const systemImg = join43(deps.androidHome, ...spec.systemImage.split(";"), "system.img");
|
|
185527
185654
|
if (deps.checkExists(systemImg)) {
|
|
185528
185655
|
ctx.ui.info(installMessages.android.systemImageAlreadyInstalled(spec.systemImage));
|
|
185529
185656
|
installedImages.add(spec.systemImage);
|
|
@@ -186575,7 +186702,7 @@ async function handleRunnerScreenshot(ctx, options, deps) {
|
|
|
186575
186702
|
}
|
|
186576
186703
|
|
|
186577
186704
|
// src/shell/interactiveRunner/collectRunFiles.ts
|
|
186578
|
-
import { join as
|
|
186705
|
+
import { join as join44 } from "node:path";
|
|
186579
186706
|
async function collectRunFiles(options) {
|
|
186580
186707
|
const paths = await glob(shippableRunFileExtensions.map((extension) => `**/*${extension}`), {
|
|
186581
186708
|
cwd: options.cwd,
|
|
@@ -186585,7 +186712,7 @@ async function collectRunFiles(options) {
|
|
|
186585
186712
|
const shippable = paths.filter(isShippableRunFilePath).sort();
|
|
186586
186713
|
const files = {};
|
|
186587
186714
|
const read = batchMap(shippable, async (path9) => ({
|
|
186588
|
-
content: await options.fs.readFile(
|
|
186715
|
+
content: await options.fs.readFile(join44(options.cwd, path9)),
|
|
186589
186716
|
path: path9
|
|
186590
186717
|
}), flowBatchSize);
|
|
186591
186718
|
for await (const file2 of read)
|
|
@@ -186600,7 +186727,7 @@ function makeRunnerId() {
|
|
|
186600
186727
|
}
|
|
186601
186728
|
|
|
186602
186729
|
// src/shell/interactiveRunner/runnerStore.ts
|
|
186603
|
-
import { join as
|
|
186730
|
+
import { join as join45 } from "node:path";
|
|
186604
186731
|
var storeFileName = "runner.json";
|
|
186605
186732
|
var storeSchema = exports_external.object({ defaultRunnerId: exports_external.string().optional() });
|
|
186606
186733
|
function parseJson(text3) {
|
|
@@ -186611,8 +186738,8 @@ function parseJson(text3) {
|
|
|
186611
186738
|
}
|
|
186612
186739
|
}
|
|
186613
186740
|
function makeRunnerStore(options) {
|
|
186614
|
-
const directory =
|
|
186615
|
-
const path9 =
|
|
186741
|
+
const directory = join45(options.cwd, qawolfDir);
|
|
186742
|
+
const path9 = join45(directory, storeFileName);
|
|
186616
186743
|
let pendingWrites = 0;
|
|
186617
186744
|
const nextPendingPath = () => `${path9}.${process.pid}.${++pendingWrites}.tmp`;
|
|
186618
186745
|
return {
|
|
@@ -186639,7 +186766,7 @@ function makeRunnerStore(options) {
|
|
|
186639
186766
|
}
|
|
186640
186767
|
|
|
186641
186768
|
// src/shell/interactiveRunner/writeScreenshot.ts
|
|
186642
|
-
import { dirname as
|
|
186769
|
+
import { dirname as dirname15 } from "node:path";
|
|
186643
186770
|
var jpegStartOfImage = [255, 216, 255];
|
|
186644
186771
|
async function writeScreenshot(options) {
|
|
186645
186772
|
const bytes = Buffer.from(options.imageJpegBase64, "base64");
|
|
@@ -186647,7 +186774,7 @@ async function writeScreenshot(options) {
|
|
|
186647
186774
|
return { ok: false, reason: "not-a-jpeg" };
|
|
186648
186775
|
}
|
|
186649
186776
|
try {
|
|
186650
|
-
await options.fs.mkdir(
|
|
186777
|
+
await options.fs.mkdir(dirname15(options.path), { recursive: true });
|
|
186651
186778
|
await options.fs.writeFile(options.path, bytes);
|
|
186652
186779
|
} catch (error51) {
|
|
186653
186780
|
return { detail: errorMessage(error51), ok: false, reason: "unwritable" };
|
|
@@ -187285,4 +187412,4 @@ createProgram({ signals }).parseAsync().catch(() => {
|
|
|
187285
187412
|
process.exitCode = 1;
|
|
187286
187413
|
}).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
187287
187414
|
|
|
187288
|
-
//# debugId=
|
|
187415
|
+
//# debugId=0187A36B3165B15F64756E2164756E21
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qawolf/cli",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.2",
|
|
4
4
|
"description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -68,7 +68,6 @@
|
|
|
68
68
|
"appium": "2.11.3",
|
|
69
69
|
"commander": "14.0.3",
|
|
70
70
|
"env-paths": "4.0.0",
|
|
71
|
-
"expect-webdriverio": "5.6.5",
|
|
72
71
|
"picomatch": "4.0.4",
|
|
73
72
|
"pino": "10.3.1",
|
|
74
73
|
"playwright": "1.62.0",
|
|
@@ -85,6 +84,7 @@
|
|
|
85
84
|
"@types/bun": "1.3.14",
|
|
86
85
|
"@types/picomatch": "4.0.3",
|
|
87
86
|
"appium-uiautomator2-driver": "4.2.9",
|
|
87
|
+
"expect-webdriverio": "5.6.5",
|
|
88
88
|
"knip": "6.16.1",
|
|
89
89
|
"oxfmt": "0.54.0",
|
|
90
90
|
"oxlint": "1.69.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qawolf-cli
|
|
3
|
-
description: Manage QA Wolf through the qawolf CLI. Use when asked
|
|
3
|
+
description: Manage QA Wolf through the qawolf CLI. Use when asked to create, update, or list coverage requests, bug reports, or maintenance reports; start a run of flows or tags on the QA Wolf platform or read a run's results; list, set, or delete environment variables; manage environments, flows, or tags; request automation of draft flows; run or list flows locally; authenticate; install the local runtime; or drive a live cloud browser (launch a runner, screenshot it, click and type on it, read its recorder) from a shell.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
compatibility: Requires the qawolf CLI on PATH. Install it from @qawolf/cli or use a standalone binary from GitHub Releases.
|
|
6
6
|
---
|