@qawolf/cli 1.17.1 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +481 -121
- package/package.json +2 -2
- package/skills/qawolf-cli/SKILL.md +17 -1
- package/skills/qawolf-cli/references/runner.md +89 -0
package/dist/cli.js
CHANGED
|
@@ -326107,6 +326107,8 @@ var interactMessages = {
|
|
|
326107
326107
|
inspectNeedsABrowserRunner: "This runner is a mobile device, and element and page HTML are a browser's shapes. Retrying will never help. Launch a playwright runner to inspect one.",
|
|
326108
326108
|
nothingToInspect: (errorMessage2) => `The runner had nothing to inspect${errorMessage2 === undefined ? "" : `: ${errorMessage2}`}. There is no live page, nothing matched the selector, or no variable has that name; a runner cannot tell those apart. Run a flow on it first, or check the selector or name.`,
|
|
326109
326109
|
inspectAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
326110
|
+
inspectMobileAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
|
|
326111
|
+
runnerIsNotMobile: "This runner is not a mobile device, so there is nothing here to inspect. Retrying will never help: launch an android or ios runner instead.",
|
|
326110
326112
|
screenNeedsARun: "This runner has not run anything yet, so its screen has never started. Waiting will not clear this and there is nothing to retry: run a flow on it with qawolf runner run, then ask again. Evaluating a snippet does not start a screen.",
|
|
326111
326113
|
screenNotReady: "The runner has a screen and cannot serve this yet. Its virtual desktop restarts when a run changes the display size, and it serves one request at a time, so something already in flight is the usual reason. Retry in a second or two.",
|
|
326112
326114
|
screenshotNotAnImage: "The screen was captured but did not arrive as a JPEG, so nothing was written. Nothing about the command needs changing: try it again, and report it if it keeps happening.",
|
|
@@ -326161,6 +326163,13 @@ var lifecycleMessages = {
|
|
|
326161
326163
|
runnerUnreachable: "The runner could not be reached. It may still be starting, or it may have terminated after inactivity. Retry, or launch it again."
|
|
326162
326164
|
};
|
|
326163
326165
|
|
|
326166
|
+
// src/core/messages/interactiveRunner/list.ts
|
|
326167
|
+
var listMessages = {
|
|
326168
|
+
noRunners: "This directory has no runner running. Launch one with qawolf runner launch, or pass --runner to address a runner started elsewhere.",
|
|
326169
|
+
runnerCount: (count) => count === 1 ? "1 runner" : `${String(count)} runners`,
|
|
326170
|
+
title: "Runners"
|
|
326171
|
+
};
|
|
326172
|
+
|
|
326164
326173
|
// src/core/messages/interactiveRunner/run.ts
|
|
326165
326174
|
var runMessages = {
|
|
326166
326175
|
envFileUnparseable: (reason) => `The --env-file could not be read. ${reason}`,
|
|
@@ -326198,6 +326207,7 @@ var runMessages = {
|
|
|
326198
326207
|
// src/core/messages/interactiveRunner/index.ts
|
|
326199
326208
|
var interactiveRunnerMessages = {
|
|
326200
326209
|
...lifecycleMessages,
|
|
326210
|
+
...listMessages,
|
|
326201
326211
|
...runMessages,
|
|
326202
326212
|
...interactMessages
|
|
326203
326213
|
};
|
|
@@ -342543,7 +342553,6 @@ var makeInspectMobileOnRunnerContract = () => {
|
|
|
342543
342553
|
output
|
|
342544
342554
|
};
|
|
342545
342555
|
};
|
|
342546
|
-
|
|
342547
342556
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/journal.js
|
|
342548
342557
|
var journalEntrySchema = exports_external.object({
|
|
342549
342558
|
payload: exports_external.unknown(),
|
|
@@ -343242,6 +343251,25 @@ var makeEvaluateSnippetOnRunnerContract = () => {
|
|
|
343242
343251
|
};
|
|
343243
343252
|
};
|
|
343244
343253
|
|
|
343254
|
+
// node_modules/@qawolf/api-contracts/dist/v1/runner/get.js
|
|
343255
|
+
var makeGetRunnerContract = () => {
|
|
343256
|
+
const input = exports_external.object({
|
|
343257
|
+
id: runnerIdSchema.describe("Id of the runner to look up.")
|
|
343258
|
+
});
|
|
343259
|
+
const output = exports_external.object({
|
|
343260
|
+
id: runnerIdSchema.describe("Id of the runner the call addressed."),
|
|
343261
|
+
outcome: exports_external.literal("success"),
|
|
343262
|
+
running: exports_external.boolean().describe("False when no runner is running under this id: it was never launched, it was terminated, or it terminated on its own after inactivity. These are not distinguished, because the run system keeps no record of a runner once it is gone.")
|
|
343263
|
+
});
|
|
343264
|
+
return {
|
|
343265
|
+
description: "Report whether a runner is running under this id on the caller's team. This is a lookup: it never starts a runner, and it does not reset a runner's inactivity clock the way reading its journal does. An id that is not running is a success with `running: false` rather than an error, so this is how to check a runner is still there before addressing it.",
|
|
343266
|
+
input,
|
|
343267
|
+
kind: "read",
|
|
343268
|
+
name: "runner.get",
|
|
343269
|
+
output
|
|
343270
|
+
};
|
|
343271
|
+
};
|
|
343272
|
+
|
|
343245
343273
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/highlightSelector.js
|
|
343246
343274
|
var maxHighlightSelectorLength = 2000;
|
|
343247
343275
|
var makeHighlightSelectorOnRunnerContract = () => {
|
|
@@ -343563,6 +343591,7 @@ var makeContractsV1 = (ids) => {
|
|
|
343563
343591
|
},
|
|
343564
343592
|
runner: {
|
|
343565
343593
|
evaluateSnippet: makeEvaluateSnippetOnRunnerContract(),
|
|
343594
|
+
get: makeGetRunnerContract(),
|
|
343566
343595
|
highlightSelector: makeHighlightSelectorOnRunnerContract(),
|
|
343567
343596
|
importPackage: makeImportPackageOnRunnerContract(),
|
|
343568
343597
|
inspect: makeInspectOnRunnerContract(),
|
|
@@ -346098,7 +346127,7 @@ function startUpdateCheck(deps) {
|
|
|
346098
346127
|
// package.json
|
|
346099
346128
|
var package_default = {
|
|
346100
346129
|
name: "@qawolf/cli",
|
|
346101
|
-
version: "1.
|
|
346130
|
+
version: "1.19.0",
|
|
346102
346131
|
description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
346103
346132
|
keywords: [
|
|
346104
346133
|
"automation",
|
|
@@ -346158,7 +346187,7 @@ var package_default = {
|
|
|
346158
346187
|
"@clack/prompts": "1.5.1",
|
|
346159
346188
|
"@napi-rs/keyring": "1.3.0",
|
|
346160
346189
|
"@oxc-node/core": "0.1.0",
|
|
346161
|
-
"@qawolf/api-contracts": "0.
|
|
346190
|
+
"@qawolf/api-contracts": "0.35.0",
|
|
346162
346191
|
"@qawolf/emails": "1.1.1",
|
|
346163
346192
|
"@qawolf/flow-targets": "1.0.0",
|
|
346164
346193
|
"@qawolf/flows": "0.1.4",
|
|
@@ -348590,28 +348619,35 @@ Examples:
|
|
|
348590
348619
|
// src/domains/flows/list.ts
|
|
348591
348620
|
import path2 from "node:path";
|
|
348592
348621
|
|
|
348593
|
-
// src/
|
|
348594
|
-
var columns = ["name", "target", "file"];
|
|
348622
|
+
// src/core/renderTable.ts
|
|
348595
348623
|
var ansiBold = "\x1B[1m";
|
|
348596
348624
|
var ansiReset = "\x1B[0m";
|
|
348597
|
-
function
|
|
348598
|
-
const
|
|
348599
|
-
|
|
348600
|
-
|
|
348601
|
-
|
|
348602
|
-
};
|
|
348603
|
-
const
|
|
348604
|
-
|
|
348605
|
-
|
|
348606
|
-
|
|
348607
|
-
|
|
348608
|
-
|
|
348609
|
-
const headerLine = boldHeader ? `${ansiBold}${renderRow(header)}${ansiReset}` : renderRow(header);
|
|
348610
|
-
return [headerLine, ...rows.map(renderRow)].join(`
|
|
348625
|
+
function renderTable(options) {
|
|
348626
|
+
const { boldHeader, columns, rows } = options;
|
|
348627
|
+
const measured = columns.map((column) => ({
|
|
348628
|
+
column,
|
|
348629
|
+
width: Math.max(column.header.length, ...rows.map((row) => column.value(row).length))
|
|
348630
|
+
}));
|
|
348631
|
+
const renderRow = (cell) => measured.map(({ column, width }) => cell(column).padEnd(width)).join(" ").trimEnd();
|
|
348632
|
+
const header = renderRow((column) => column.header);
|
|
348633
|
+
return [
|
|
348634
|
+
boldHeader ? `${ansiBold}${header}${ansiReset}` : header,
|
|
348635
|
+
...rows.map((row) => renderRow((column) => column.value(row)))
|
|
348636
|
+
].join(`
|
|
348611
348637
|
`) + `
|
|
348612
348638
|
`;
|
|
348613
348639
|
}
|
|
348614
348640
|
|
|
348641
|
+
// src/domains/flows/renderListTable.ts
|
|
348642
|
+
var columns = [
|
|
348643
|
+
{ header: "name", value: (row) => row.name },
|
|
348644
|
+
{ header: "target", value: (row) => row.target },
|
|
348645
|
+
{ header: "file", value: (row) => row.file }
|
|
348646
|
+
];
|
|
348647
|
+
function renderListTable(rows, boldHeader) {
|
|
348648
|
+
return renderTable({ boldHeader, columns, rows });
|
|
348649
|
+
}
|
|
348650
|
+
|
|
348615
348651
|
// src/domains/flows/list.ts
|
|
348616
348652
|
async function flowsList(ctx, pattern, deps) {
|
|
348617
348653
|
const patterns = pattern ? [pattern] : [];
|
|
@@ -348672,7 +348708,8 @@ async function flowsListRemote(ctx, pattern, options) {
|
|
|
348672
348708
|
file: f2.path,
|
|
348673
348709
|
name: f2.name,
|
|
348674
348710
|
tags: f2.tags,
|
|
348675
|
-
target: typeof f2.executionTarget === "string" ? f2.executionTarget : JSON.stringify(f2.executionTarget)
|
|
348711
|
+
target: typeof f2.executionTarget === "string" ? f2.executionTarget : JSON.stringify(f2.executionTarget),
|
|
348712
|
+
url: f2.url
|
|
348676
348713
|
}));
|
|
348677
348714
|
if (ctx.ui.mode === "json") {
|
|
348678
348715
|
ctx.ui.json(items);
|
|
@@ -356592,6 +356629,7 @@ async function handlePublicApiCommand(ctx, spec, options) {
|
|
|
356592
356629
|
var skippedContractNames = new Set([
|
|
356593
356630
|
"flow.list",
|
|
356594
356631
|
"runner.evaluateSnippet",
|
|
356632
|
+
"runner.get",
|
|
356595
356633
|
"runner.highlightSelector",
|
|
356596
356634
|
"runner.importPackage",
|
|
356597
356635
|
"runner.inspect",
|
|
@@ -356660,21 +356698,7 @@ function registerPublicApiCommands(program2, signals, options = {}) {
|
|
|
356660
356698
|
// src/domains/interactiveRunner/runnerCallOptions.ts
|
|
356661
356699
|
var runnerCallOptions = { timeoutMs: 90000 };
|
|
356662
356700
|
|
|
356663
|
-
// src/domains/interactiveRunner/
|
|
356664
|
-
function parseRunnerId(id) {
|
|
356665
|
-
const parsed = runnerIdSchema.safeParse(id);
|
|
356666
|
-
if (!parsed.success)
|
|
356667
|
-
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
356668
|
-
return { id: parsed.data, ok: true };
|
|
356669
|
-
}
|
|
356670
|
-
function parseRunnerName(runnerName) {
|
|
356671
|
-
const parsed = runnerNameSchema.safeParse(runnerName);
|
|
356672
|
-
if (!parsed.success)
|
|
356673
|
-
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
356674
|
-
return { ok: true, runnerName: parsed.data };
|
|
356675
|
-
}
|
|
356676
|
-
|
|
356677
|
-
// src/domains/interactiveRunner/launch.ts
|
|
356701
|
+
// src/domains/interactiveRunner/launchAndRemember.ts
|
|
356678
356702
|
async function launchRunner(ctx, options) {
|
|
356679
356703
|
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.launch, {
|
|
356680
356704
|
id: options.id,
|
|
@@ -356694,38 +356718,50 @@ async function launchAndRemember(ctx, options, deps) {
|
|
|
356694
356718
|
const priorDefault = await deps.store.readDefaultRunnerId().catch(() => {
|
|
356695
356719
|
return;
|
|
356696
356720
|
});
|
|
356697
|
-
const remembered = await deps.store.
|
|
356721
|
+
const remembered = await deps.store.rememberLaunch({ id: options.id, runnerName: options.runnerName }).then(() => true).catch(() => {
|
|
356698
356722
|
ctx.ui.warn(interactiveRunnerMessages.defaultNotRemembered(options.id));
|
|
356699
356723
|
return false;
|
|
356700
356724
|
});
|
|
356701
356725
|
const launched = await launchRunner(ctx, options);
|
|
356702
|
-
if (launched.ok)
|
|
356726
|
+
if (launched.ok) {
|
|
356727
|
+
if (remembered) {
|
|
356728
|
+
await deps.store.rememberLaunch({
|
|
356729
|
+
id: options.id,
|
|
356730
|
+
runnerName: launched.value.runnerName
|
|
356731
|
+
}).catch(() => {
|
|
356732
|
+
return;
|
|
356733
|
+
});
|
|
356734
|
+
}
|
|
356703
356735
|
return launched;
|
|
356736
|
+
}
|
|
356704
356737
|
if (remembered && !launched.mayHaveArrived) {
|
|
356705
|
-
await
|
|
356738
|
+
await deps.store.forgetRunner(options.id).catch(() => {
|
|
356706
356739
|
return;
|
|
356707
356740
|
});
|
|
356741
|
+
if (priorDefault !== undefined) {
|
|
356742
|
+
await deps.store.writeDefaultRunnerId(priorDefault).catch(() => {
|
|
356743
|
+
return;
|
|
356744
|
+
});
|
|
356745
|
+
}
|
|
356708
356746
|
}
|
|
356709
356747
|
return {
|
|
356710
356748
|
...launched,
|
|
356711
356749
|
error: launched.mayHaveArrived ? interactiveRunnerMessages.launchLost(options.id, launched.error) : interactiveRunnerMessages.launchFailed(options.id, launched.error)
|
|
356712
356750
|
};
|
|
356713
356751
|
}
|
|
356714
|
-
|
|
356715
|
-
|
|
356716
|
-
|
|
356717
|
-
|
|
356718
|
-
|
|
356719
|
-
|
|
356720
|
-
|
|
356721
|
-
|
|
356722
|
-
|
|
356723
|
-
const
|
|
356724
|
-
if (!
|
|
356725
|
-
return {
|
|
356726
|
-
}
|
|
356727
|
-
ctx.ui.output(launched.value, launched.value.alreadyRunning ? interactiveRunnerMessages.alreadyRunning(launched.value.id) : interactiveRunnerMessages.launched(launched.value.id));
|
|
356728
|
-
return;
|
|
356752
|
+
|
|
356753
|
+
// src/domains/interactiveRunner/runnerIds.ts
|
|
356754
|
+
function parseRunnerId(id) {
|
|
356755
|
+
const parsed = runnerIdSchema.safeParse(id);
|
|
356756
|
+
if (!parsed.success)
|
|
356757
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
356758
|
+
return { id: parsed.data, ok: true };
|
|
356759
|
+
}
|
|
356760
|
+
function parseRunnerName(runnerName) {
|
|
356761
|
+
const parsed = runnerNameSchema.safeParse(runnerName);
|
|
356762
|
+
if (!parsed.success)
|
|
356763
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
356764
|
+
return { ok: true, runnerName: parsed.data };
|
|
356729
356765
|
}
|
|
356730
356766
|
|
|
356731
356767
|
// src/domains/interactiveRunner/resolveRunner.ts
|
|
@@ -357077,7 +357113,14 @@ function parseJson(text3) {
|
|
|
357077
357113
|
// src/shell/interactiveRunner/runnerStore.ts
|
|
357078
357114
|
import { join as join47 } from "node:path";
|
|
357079
357115
|
var storeFileName = "runner.json";
|
|
357080
|
-
var
|
|
357116
|
+
var runnersDirName = "runners";
|
|
357117
|
+
var storedRunnerSchema = exports_external.object({
|
|
357118
|
+
id: exports_external.string(),
|
|
357119
|
+
runnerName: exports_external.string().optional()
|
|
357120
|
+
});
|
|
357121
|
+
var storeSchema = exports_external.object({
|
|
357122
|
+
defaultRunnerId: exports_external.string().optional()
|
|
357123
|
+
});
|
|
357081
357124
|
function parseJson2(text3) {
|
|
357082
357125
|
try {
|
|
357083
357126
|
return JSON.parse(text3);
|
|
@@ -357085,30 +357128,81 @@ function parseJson2(text3) {
|
|
|
357085
357128
|
return;
|
|
357086
357129
|
}
|
|
357087
357130
|
}
|
|
357131
|
+
var pendingWrites = 0;
|
|
357088
357132
|
function makeRunnerStore(options) {
|
|
357089
357133
|
const directory = join47(options.cwd, qawolfDir);
|
|
357090
357134
|
const path9 = join47(directory, storeFileName);
|
|
357091
|
-
|
|
357092
|
-
const nextPendingPath = () => `${
|
|
357135
|
+
const runnersDir = join47(directory, runnersDirName);
|
|
357136
|
+
const nextPendingPath = (target) => `${target}.${process.pid}.${++pendingWrites}.tmp`;
|
|
357137
|
+
const runnerPath = (runnerId) => join47(runnersDir, `${encodeURIComponent(runnerId)}.json`);
|
|
357138
|
+
const writeAtomically = async (target, contents2) => {
|
|
357139
|
+
const pendingPath = nextPendingPath(target);
|
|
357140
|
+
await options.fs.writeFile(pendingPath, `${JSON.stringify(contents2, undefined, 2)}
|
|
357141
|
+
`);
|
|
357142
|
+
await options.fs.rename(pendingPath, target);
|
|
357143
|
+
};
|
|
357144
|
+
const readDefaultRunnerId = async () => {
|
|
357145
|
+
const contents2 = await options.fs.readFile(path9).catch(() => {
|
|
357146
|
+
return;
|
|
357147
|
+
});
|
|
357148
|
+
if (contents2 === undefined)
|
|
357149
|
+
return;
|
|
357150
|
+
const parsed = storeSchema.safeParse(parseJson2(contents2));
|
|
357151
|
+
return parsed.success ? parsed.data.defaultRunnerId : undefined;
|
|
357152
|
+
};
|
|
357153
|
+
const writeDefaultRunnerId = async (runnerId) => {
|
|
357154
|
+
await options.fs.mkdir(directory, { recursive: true });
|
|
357155
|
+
await writeAtomically(path9, { defaultRunnerId: runnerId });
|
|
357156
|
+
};
|
|
357157
|
+
const readRunnerFileNames = async () => {
|
|
357158
|
+
try {
|
|
357159
|
+
const names = await options.fs.readdir(runnersDir);
|
|
357160
|
+
return names.filter((name) => name.endsWith(".json"));
|
|
357161
|
+
} catch {
|
|
357162
|
+
return [];
|
|
357163
|
+
}
|
|
357164
|
+
};
|
|
357093
357165
|
return {
|
|
357094
|
-
async
|
|
357095
|
-
await options.fs.rm(
|
|
357166
|
+
async forgetRunner(runnerId) {
|
|
357167
|
+
await options.fs.rm(runnerPath(runnerId), { force: true });
|
|
357168
|
+
if (await readDefaultRunnerId() === runnerId)
|
|
357169
|
+
await writeDefaultRunnerId(undefined);
|
|
357096
357170
|
},
|
|
357097
|
-
|
|
357098
|
-
|
|
357099
|
-
|
|
357100
|
-
|
|
357101
|
-
|
|
357102
|
-
|
|
357103
|
-
|
|
357104
|
-
|
|
357171
|
+
readDefaultRunnerId,
|
|
357172
|
+
async readRunners() {
|
|
357173
|
+
const runners = await Promise.all((await readRunnerFileNames()).map(async (name) => {
|
|
357174
|
+
const contents2 = await options.fs.readFile(join47(runnersDir, name)).catch(() => {
|
|
357175
|
+
return;
|
|
357176
|
+
});
|
|
357177
|
+
if (contents2 === undefined)
|
|
357178
|
+
return;
|
|
357179
|
+
const parsed = storedRunnerSchema.safeParse(parseJson2(contents2));
|
|
357180
|
+
return parsed.success ? parsed.data : undefined;
|
|
357181
|
+
}));
|
|
357182
|
+
return runners.filter((runner) => !!runner).sort((left, right) => left.id.localeCompare(right.id));
|
|
357183
|
+
},
|
|
357184
|
+
async rememberLaunch(runner) {
|
|
357185
|
+
await options.fs.mkdir(runnersDir, { recursive: true });
|
|
357186
|
+
await writeAtomically(runnerPath(runner.id), runner);
|
|
357187
|
+
await writeDefaultRunnerId(runner.id);
|
|
357188
|
+
},
|
|
357189
|
+
async dropRunners(runnerIds) {
|
|
357190
|
+
await Promise.all(runnerIds.map((runnerId) => options.fs.rm(runnerPath(runnerId), { force: true })));
|
|
357191
|
+
const names = await readRunnerFileNames();
|
|
357192
|
+
await Promise.all(names.map(async (name) => {
|
|
357193
|
+
const runnerFile = join47(runnersDir, name);
|
|
357194
|
+
const contents2 = await options.fs.readFile(runnerFile).catch(() => {
|
|
357195
|
+
return;
|
|
357196
|
+
});
|
|
357197
|
+
if (contents2 === undefined)
|
|
357198
|
+
return;
|
|
357199
|
+
if (storedRunnerSchema.safeParse(parseJson2(contents2)).success)
|
|
357200
|
+
return;
|
|
357201
|
+
await options.fs.rm(runnerFile, { force: true });
|
|
357202
|
+
}));
|
|
357105
357203
|
},
|
|
357106
357204
|
async writeDefaultRunnerId(runnerId) {
|
|
357107
|
-
|
|
357108
|
-
await options.fs.mkdir(directory, { recursive: true });
|
|
357109
|
-
await options.fs.writeFile(pendingPath, `${JSON.stringify({ defaultRunnerId: runnerId }, undefined, 2)}
|
|
357110
|
-
`);
|
|
357111
|
-
await options.fs.rename(pendingPath, path9);
|
|
357205
|
+
await writeDefaultRunnerId(runnerId);
|
|
357112
357206
|
}
|
|
357113
357207
|
};
|
|
357114
357208
|
}
|
|
@@ -357353,14 +357447,190 @@ async function handleRunnerInspect(ctx, options, deps) {
|
|
|
357353
357447
|
return;
|
|
357354
357448
|
}
|
|
357355
357449
|
|
|
357450
|
+
// src/core/interactiveRunner/inspectMobileRequest.ts
|
|
357451
|
+
function toNumber(value) {
|
|
357452
|
+
return value.trim() === "" ? Number.NaN : Number(value);
|
|
357453
|
+
}
|
|
357454
|
+
function buildInspectMobileRequest(what, flags) {
|
|
357455
|
+
if (flags.by === "point" && (flags.text !== undefined || flags.partial !== undefined)) {
|
|
357456
|
+
return {
|
|
357457
|
+
error: "--by point matches by pixel, so --text/--partial would be ignored rather than searching by text. Pass --by text instead, or drop --text/--partial.",
|
|
357458
|
+
ok: false
|
|
357459
|
+
};
|
|
357460
|
+
}
|
|
357461
|
+
if (flags.by === "text" && (flags.x !== undefined || flags.y !== undefined)) {
|
|
357462
|
+
return {
|
|
357463
|
+
error: "--by text matches by text, so --x/--y would be ignored rather than matching a point. Pass --by point instead, or drop --x/--y.",
|
|
357464
|
+
ok: false
|
|
357465
|
+
};
|
|
357466
|
+
}
|
|
357467
|
+
const candidate = {
|
|
357468
|
+
what,
|
|
357469
|
+
...flags.by === undefined ? {} : { by: flags.by },
|
|
357470
|
+
...flags.context === undefined ? {} : { context: flags.context },
|
|
357471
|
+
...flags.partial === undefined ? {} : { partial: flags.partial },
|
|
357472
|
+
...flags.text === undefined ? {} : { text: flags.text },
|
|
357473
|
+
...flags.x === undefined ? {} : { x: toNumber(flags.x) },
|
|
357474
|
+
...flags.y === undefined ? {} : { y: toNumber(flags.y) }
|
|
357475
|
+
};
|
|
357476
|
+
const parsed = inspectMobileRequestSchema.safeParse(candidate);
|
|
357477
|
+
if (!parsed.success) {
|
|
357478
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
357479
|
+
}
|
|
357480
|
+
return { ok: true, request: parsed.data };
|
|
357481
|
+
}
|
|
357482
|
+
|
|
357483
|
+
// src/domains/interactiveRunner/inspectMobile.ts
|
|
357484
|
+
function describeSession(session) {
|
|
357485
|
+
switch (session.type) {
|
|
357486
|
+
case "ready":
|
|
357487
|
+
return session.deviceName === undefined ? `Session ready: ${session.platformName} (${session.sessionId}).` : `Session ready: ${session.platformName} on ${session.deviceName} (${session.sessionId}).`;
|
|
357488
|
+
case "unreachable":
|
|
357489
|
+
return `Session unreachable: ${session.error}`;
|
|
357490
|
+
case "ambiguous":
|
|
357491
|
+
return `${String(session.sessionCount)} Appium sessions are live; expected one.`;
|
|
357492
|
+
case "no-session":
|
|
357493
|
+
return "No Appium session is live.";
|
|
357494
|
+
}
|
|
357495
|
+
}
|
|
357496
|
+
function streamLine(value) {
|
|
357497
|
+
switch (value.what) {
|
|
357498
|
+
case "contexts":
|
|
357499
|
+
return JSON.stringify({
|
|
357500
|
+
contexts: value.contexts,
|
|
357501
|
+
current: value.current
|
|
357502
|
+
});
|
|
357503
|
+
case "page":
|
|
357504
|
+
return JSON.stringify({
|
|
357505
|
+
context: value.context,
|
|
357506
|
+
orientation: value.orientation,
|
|
357507
|
+
pageSource: value.pageSource
|
|
357508
|
+
});
|
|
357509
|
+
case "elements":
|
|
357510
|
+
return JSON.stringify({ matches: value.matches });
|
|
357511
|
+
}
|
|
357512
|
+
}
|
|
357513
|
+
async function handleRunnerInspectMobile(ctx, options, deps) {
|
|
357514
|
+
const built = buildInspectMobileRequest(options.what, options.flags);
|
|
357515
|
+
if (!built.ok)
|
|
357516
|
+
return { error: built.error, exitCode: exitCodes.invalidArgs };
|
|
357517
|
+
const resolved = await resolveRunner(ctx, {
|
|
357518
|
+
autoLaunch: false,
|
|
357519
|
+
noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForInspect,
|
|
357520
|
+
runner: options.runner
|
|
357521
|
+
}, deps);
|
|
357522
|
+
if (resolved.type === "failed") {
|
|
357523
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
357524
|
+
}
|
|
357525
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.inspectMobile, { id: resolved.runnerId, request: built.request }, runnerCallOptions);
|
|
357526
|
+
if (!result.ok) {
|
|
357527
|
+
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
357528
|
+
}
|
|
357529
|
+
if (result.value.outcome === "success") {
|
|
357530
|
+
const value = result.value;
|
|
357531
|
+
if (value.what === "session") {
|
|
357532
|
+
ctx.ui.output(value, describeSession(value.session));
|
|
357533
|
+
} else {
|
|
357534
|
+
ctx.ui.stream(value, streamLine(value));
|
|
357535
|
+
}
|
|
357536
|
+
return;
|
|
357537
|
+
}
|
|
357538
|
+
const { failureReason } = result.value;
|
|
357539
|
+
switch (failureReason) {
|
|
357540
|
+
case "runner-is-not-mobile":
|
|
357541
|
+
return {
|
|
357542
|
+
error: interactiveRunnerMessages.runnerIsNotMobile,
|
|
357543
|
+
exitCode: exitCodes.invalidArgs
|
|
357544
|
+
};
|
|
357545
|
+
case "screen-needs-a-run":
|
|
357546
|
+
return {
|
|
357547
|
+
error: interactiveRunnerMessages.screenNeedsARun,
|
|
357548
|
+
exitCode: exitCodes.invalidArgs
|
|
357549
|
+
};
|
|
357550
|
+
case "screen-not-ready":
|
|
357551
|
+
return {
|
|
357552
|
+
error: interactiveRunnerMessages.screenNotReady,
|
|
357553
|
+
exitCode: exitCodes.network
|
|
357554
|
+
};
|
|
357555
|
+
case "runner-unreachable":
|
|
357556
|
+
return {
|
|
357557
|
+
error: interactiveRunnerMessages.runnerUnreachable,
|
|
357558
|
+
exitCode: exitCodes.network
|
|
357559
|
+
};
|
|
357560
|
+
default: {
|
|
357561
|
+
return {
|
|
357562
|
+
error: interactiveRunnerMessages.inspectMobileAnsweredUnknown(failureReason),
|
|
357563
|
+
exitCode: exitCodes.network
|
|
357564
|
+
};
|
|
357565
|
+
}
|
|
357566
|
+
}
|
|
357567
|
+
}
|
|
357568
|
+
|
|
357569
|
+
// src/commands/runner/inspectMobile.register.ts
|
|
357570
|
+
function registerRunnerInspectMobileCommands(inspect, signals) {
|
|
357571
|
+
declareCommandKind(inspect.command("session"), "read").description("Print the Appium session's status: ready, or why not").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
|
|
357572
|
+
flags: {
|
|
357573
|
+
by: undefined,
|
|
357574
|
+
context: undefined,
|
|
357575
|
+
partial: undefined,
|
|
357576
|
+
text: undefined,
|
|
357577
|
+
x: undefined,
|
|
357578
|
+
y: undefined
|
|
357579
|
+
},
|
|
357580
|
+
runner: opts.runner,
|
|
357581
|
+
what: "session"
|
|
357582
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
357583
|
+
declareCommandKind(inspect.command("contexts"), "read").description("List the WebView contexts available, and which is current").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
|
|
357584
|
+
flags: {
|
|
357585
|
+
by: undefined,
|
|
357586
|
+
context: undefined,
|
|
357587
|
+
partial: undefined,
|
|
357588
|
+
text: undefined,
|
|
357589
|
+
x: undefined,
|
|
357590
|
+
y: undefined
|
|
357591
|
+
},
|
|
357592
|
+
runner: opts.runner,
|
|
357593
|
+
what: "contexts"
|
|
357594
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
357595
|
+
declareCommandKind(inspect.command("page-source"), "read").description("Print the current context's page source, as a tree").option("--context <name>", "Read this context instead of the current one").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
|
|
357596
|
+
flags: {
|
|
357597
|
+
by: undefined,
|
|
357598
|
+
context: opts.context,
|
|
357599
|
+
partial: undefined,
|
|
357600
|
+
text: undefined,
|
|
357601
|
+
x: undefined,
|
|
357602
|
+
y: undefined
|
|
357603
|
+
},
|
|
357604
|
+
runner: opts.runner,
|
|
357605
|
+
what: "page"
|
|
357606
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
357607
|
+
declareCommandKind(inspect.command("elements"), "read").description("Find elements at a screen point, or elements carrying some text").requiredOption("--by <by>", "point or text").option("--context <name>", "Read this context instead of the current one").option("--partial", "text: match text containing this, rather than exactly this").option("--runner <id>", runnerFlagDescription).option("--text <text>", "text: the text to match").option("--x <pixels>", "point: whole pixels on the device's own screen").option("--y <pixels>", "point: whole pixels on the device's own screen").action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
|
|
357608
|
+
flags: {
|
|
357609
|
+
by: opts.by,
|
|
357610
|
+
context: opts.context,
|
|
357611
|
+
partial: opts.partial,
|
|
357612
|
+
text: opts.text,
|
|
357613
|
+
x: opts.x,
|
|
357614
|
+
y: opts.y
|
|
357615
|
+
},
|
|
357616
|
+
runner: opts.runner,
|
|
357617
|
+
what: "elements"
|
|
357618
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
357619
|
+
}
|
|
357620
|
+
|
|
357356
357621
|
// src/commands/runner/inspect.register.ts
|
|
357357
357622
|
var inspectExamples = `
|
|
357358
357623
|
Examples:
|
|
357359
357624
|
$ qawolf runner inspect element-html --selector "#email"
|
|
357360
357625
|
$ qawolf runner inspect page-html > page.html
|
|
357361
|
-
$ qawolf runner inspect variable --name cart | jq .total
|
|
357626
|
+
$ qawolf runner inspect variable --name cart | jq .total
|
|
357627
|
+
$ qawolf runner inspect session
|
|
357628
|
+
$ qawolf runner inspect contexts
|
|
357629
|
+
$ qawolf runner inspect page-source --context WEBVIEW_1
|
|
357630
|
+
$ qawolf runner inspect elements --by point --x 200 --y 400
|
|
357631
|
+
$ qawolf runner inspect elements --by text --text "Sign in" --partial`;
|
|
357362
357632
|
function registerRunnerInspectCommands(runner, signals) {
|
|
357363
|
-
const inspect = runner.command("inspect").description("Read one thing off a runner's live page").addHelpText("after", inspectExamples);
|
|
357633
|
+
const inspect = runner.command("inspect").description("Read one thing off a runner's live page (browser) or Appium session (mobile)").addHelpText("after", inspectExamples);
|
|
357364
357634
|
declareCommandKind(inspect.command("element-html"), "read").description("Print the HTML of the first element a selector matches").requiredOption("--selector <selector>", "Playwright selector to inspect").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspect(ctx, {
|
|
357365
357635
|
flags: { name: undefined, selector: opts.selector },
|
|
357366
357636
|
runner: opts.runner,
|
|
@@ -357376,6 +357646,7 @@ function registerRunnerInspectCommands(runner, signals) {
|
|
|
357376
357646
|
runner: opts.runner,
|
|
357377
357647
|
what: "variable"
|
|
357378
357648
|
}, runnerDeps(ctx)))(opts, command));
|
|
357649
|
+
registerRunnerInspectMobileCommands(inspect, signals);
|
|
357379
357650
|
}
|
|
357380
357651
|
|
|
357381
357652
|
// src/core/interactiveRunner/runFiles.ts
|
|
@@ -357513,46 +357784,6 @@ async function handleRunnerExec(ctx, options, deps) {
|
|
|
357513
357784
|
}
|
|
357514
357785
|
}
|
|
357515
357786
|
|
|
357516
|
-
// src/core/interactiveRunner/browserAction.ts
|
|
357517
|
-
function parseJsonPath(path9) {
|
|
357518
|
-
try {
|
|
357519
|
-
return { ok: true, value: JSON.parse(path9) };
|
|
357520
|
-
} catch {
|
|
357521
|
-
return {
|
|
357522
|
-
error: `--path must be a JSON array of points, for example '[{"x":10,"y":20},{"x":80,"y":90}]'.`,
|
|
357523
|
-
ok: false
|
|
357524
|
-
};
|
|
357525
|
-
}
|
|
357526
|
-
}
|
|
357527
|
-
function toNumber(value) {
|
|
357528
|
-
return value.trim() === "" ? Number.NaN : Number(value);
|
|
357529
|
-
}
|
|
357530
|
-
function buildBrowserAction(type, flags) {
|
|
357531
|
-
const parsedPath = flags.path === undefined ? undefined : parseJsonPath(flags.path);
|
|
357532
|
-
if (parsedPath !== undefined && !parsedPath.ok)
|
|
357533
|
-
return parsedPath;
|
|
357534
|
-
const candidate = {
|
|
357535
|
-
type,
|
|
357536
|
-
...flags.button === undefined ? {} : { button: flags.button },
|
|
357537
|
-
...flags.keys === undefined ? {} : { keys: flags.keys },
|
|
357538
|
-
...parsedPath === undefined ? {} : { path: parsedPath.value },
|
|
357539
|
-
...flags.scrollX === undefined ? {} : { scroll_x: toNumber(flags.scrollX) },
|
|
357540
|
-
...flags.scrollY === undefined ? {} : { scroll_y: toNumber(flags.scrollY) },
|
|
357541
|
-
...flags.text === undefined ? {} : { text: flags.text },
|
|
357542
|
-
...flags.url === undefined ? {} : { url: flags.url },
|
|
357543
|
-
...flags.x === undefined ? {} : { x: toNumber(flags.x) },
|
|
357544
|
-
...flags.y === undefined ? {} : { y: toNumber(flags.y) }
|
|
357545
|
-
};
|
|
357546
|
-
return parseBrowserAction(candidate);
|
|
357547
|
-
}
|
|
357548
|
-
function parseBrowserAction(candidate) {
|
|
357549
|
-
const parsed = browserActionSchema.safeParse(candidate);
|
|
357550
|
-
if (!parsed.success) {
|
|
357551
|
-
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
357552
|
-
}
|
|
357553
|
-
return { action: parsed.data, ok: true };
|
|
357554
|
-
}
|
|
357555
|
-
|
|
357556
357787
|
// src/domains/interactiveRunner/performActionFailure.ts
|
|
357557
357788
|
function describePerformActionFailure(options) {
|
|
357558
357789
|
const { actionType, failure } = options;
|
|
@@ -357597,7 +357828,47 @@ function describePerformActionFailure(options) {
|
|
|
357597
357828
|
}
|
|
357598
357829
|
}
|
|
357599
357830
|
|
|
357600
|
-
// src/
|
|
357831
|
+
// src/core/interactiveRunner/browserAction.ts
|
|
357832
|
+
function parseJsonPath(path9) {
|
|
357833
|
+
try {
|
|
357834
|
+
return { ok: true, value: JSON.parse(path9) };
|
|
357835
|
+
} catch {
|
|
357836
|
+
return {
|
|
357837
|
+
error: `--path must be a JSON array of points, for example '[{"x":10,"y":20},{"x":80,"y":90}]'.`,
|
|
357838
|
+
ok: false
|
|
357839
|
+
};
|
|
357840
|
+
}
|
|
357841
|
+
}
|
|
357842
|
+
function toNumber2(value) {
|
|
357843
|
+
return value.trim() === "" ? Number.NaN : Number(value);
|
|
357844
|
+
}
|
|
357845
|
+
function buildBrowserAction(type, flags) {
|
|
357846
|
+
const parsedPath = flags.path === undefined ? undefined : parseJsonPath(flags.path);
|
|
357847
|
+
if (parsedPath !== undefined && !parsedPath.ok)
|
|
357848
|
+
return parsedPath;
|
|
357849
|
+
const candidate = {
|
|
357850
|
+
type,
|
|
357851
|
+
...flags.button === undefined ? {} : { button: flags.button },
|
|
357852
|
+
...flags.keys === undefined ? {} : { keys: flags.keys },
|
|
357853
|
+
...parsedPath === undefined ? {} : { path: parsedPath.value },
|
|
357854
|
+
...flags.scrollX === undefined ? {} : { scroll_x: toNumber2(flags.scrollX) },
|
|
357855
|
+
...flags.scrollY === undefined ? {} : { scroll_y: toNumber2(flags.scrollY) },
|
|
357856
|
+
...flags.text === undefined ? {} : { text: flags.text },
|
|
357857
|
+
...flags.url === undefined ? {} : { url: flags.url },
|
|
357858
|
+
...flags.x === undefined ? {} : { x: toNumber2(flags.x) },
|
|
357859
|
+
...flags.y === undefined ? {} : { y: toNumber2(flags.y) }
|
|
357860
|
+
};
|
|
357861
|
+
return parseBrowserAction(candidate);
|
|
357862
|
+
}
|
|
357863
|
+
function parseBrowserAction(candidate) {
|
|
357864
|
+
const parsed = browserActionSchema.safeParse(candidate);
|
|
357865
|
+
if (!parsed.success) {
|
|
357866
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
357867
|
+
}
|
|
357868
|
+
return { action: parsed.data, ok: true };
|
|
357869
|
+
}
|
|
357870
|
+
|
|
357871
|
+
// src/domains/interactiveRunner/readAction.ts
|
|
357601
357872
|
var stdinArgument2 = "-";
|
|
357602
357873
|
async function readAction(type, flags, deps) {
|
|
357603
357874
|
if (type !== stdinArgument2)
|
|
@@ -357615,6 +357886,8 @@ async function readAction(type, flags, deps) {
|
|
|
357615
357886
|
return { error: interactiveRunnerMessages.actionNotJson, ok: false };
|
|
357616
357887
|
}
|
|
357617
357888
|
}
|
|
357889
|
+
|
|
357890
|
+
// src/domains/interactiveRunner/performAction.ts
|
|
357618
357891
|
async function handleRunnerAct(ctx, options, deps) {
|
|
357619
357892
|
const built = await readAction(options.type, options.flags, deps);
|
|
357620
357893
|
if (!built.ok)
|
|
@@ -357728,7 +358001,7 @@ Examples:
|
|
|
357728
358001
|
$ qawolf runner exec snippet.ts --file flows/checkout.flow.ts`;
|
|
357729
358002
|
function registerRunnerInteractCommands(runner, signals) {
|
|
357730
358003
|
declareCommandKind(runner.command("screenshot"), "read").description("Save a JPEG of an interactive runner's screen to a file").option("--out <path>", "File to write the image to", defaultScreenshotPath).option("--runner <id>", runnerFlagDescription).addHelpText("after", screenshotExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerScreenshot(ctx, { out: opts.out, runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
357731
|
-
declareCommandKind(runner.command("act <action>"), "write").description("Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin").option("--button <button>", "click: left, right, wheel, back or forward").option("--keys <keys...>", "keypress: modifiers and the key, e.g. Control a").option("--path <json>", "drag: JSON array of points to drag through").option("--runner <id>", runnerFlagDescription).option("--scroll-x <delta>", "scroll: horizontal wheel delta").option("--scroll-y <delta>", "scroll: vertical wheel delta").option("--text <text>", "type: the text to type").option("--url <url>", "navigate: the http or https URL to go to").option("--x <pixels>", "pointer x, in screenshot pixels").option("--y <pixels>", "pointer y, in screenshot pixels").addHelpText("after", actExamples).action((action2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
|
|
358004
|
+
declareCommandKind(runner.command("act <action>"), "write").description("Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin. On a mobile runner only click (button left), drag and type have a touchscreen equivalent; the rest answer action-not-supported-on-mobile").option("--button <button>", "click: left, right, wheel, back or forward (mobile: left only)").option("--keys <keys...>", "keypress: modifiers and the key, e.g. Control a").option("--path <json>", "drag: JSON array of points to drag through (mobile: only the first and last are used)").option("--runner <id>", runnerFlagDescription).option("--scroll-x <delta>", "scroll: horizontal wheel delta").option("--scroll-y <delta>", "scroll: vertical wheel delta").option("--text <text>", "type: the text to type").option("--url <url>", "navigate: the http or https URL to go to").option("--x <pixels>", "pointer x, in screenshot pixels").option("--y <pixels>", "pointer y, in screenshot pixels").addHelpText("after", actExamples).action((action2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
|
|
357732
358005
|
flags: {
|
|
357733
358006
|
button: opts.button,
|
|
357734
358007
|
keys: opts.keys,
|
|
@@ -357795,6 +358068,92 @@ async function handleRunnerKeepalive(ctx, options, deps) {
|
|
|
357795
358068
|
return;
|
|
357796
358069
|
}
|
|
357797
358070
|
|
|
358071
|
+
// src/domains/interactiveRunner/launch.ts
|
|
358072
|
+
async function handleRunnerLaunch(ctx, options, deps) {
|
|
358073
|
+
const chosenId = options.id ?? deps.makeRunnerId();
|
|
358074
|
+
const id = parseRunnerId(chosenId);
|
|
358075
|
+
if (!id.ok)
|
|
358076
|
+
return { error: id.error, exitCode: exitCodes.invalidArgs };
|
|
358077
|
+
const runnerName = options.name === undefined ? undefined : parseRunnerName(options.name);
|
|
358078
|
+
if (runnerName !== undefined && !runnerName.ok) {
|
|
358079
|
+
return { error: runnerName.error, exitCode: exitCodes.invalidArgs };
|
|
358080
|
+
}
|
|
358081
|
+
const launched = await launchAndRemember(ctx, { id: id.id, runnerName: runnerName?.runnerName }, deps);
|
|
358082
|
+
if (!launched.ok) {
|
|
358083
|
+
return { ...failureFields(launched), exitCode: launched.exitCode };
|
|
358084
|
+
}
|
|
358085
|
+
ctx.ui.output(launched.value, launched.value.alreadyRunning ? interactiveRunnerMessages.alreadyRunning(launched.value.id) : interactiveRunnerMessages.launched(launched.value.id));
|
|
358086
|
+
return;
|
|
358087
|
+
}
|
|
358088
|
+
|
|
358089
|
+
// src/domains/interactiveRunner/list.ts
|
|
358090
|
+
var columns2 = [
|
|
358091
|
+
{ header: "id", value: (row) => row.id },
|
|
358092
|
+
{ header: "family", value: (row) => row.runnerName ?? "" },
|
|
358093
|
+
{ header: "default", value: (row) => row.isDefault ? "yes" : "" }
|
|
358094
|
+
];
|
|
358095
|
+
var probeBatchSize = 8;
|
|
358096
|
+
async function readCandidates(deps) {
|
|
358097
|
+
const held = await deps.store.readRunners();
|
|
358098
|
+
const storedDefault = await deps.store.readDefaultRunnerId();
|
|
358099
|
+
const environmentValue = deps.env[runnerIdEnvironmentVariable]?.trim();
|
|
358100
|
+
const fromEnvironment = environmentValue ? environmentValue : undefined;
|
|
358101
|
+
const candidates = [...held];
|
|
358102
|
+
for (const id of [storedDefault, fromEnvironment]) {
|
|
358103
|
+
if (id !== undefined && !candidates.some((runner) => runner.id === id)) {
|
|
358104
|
+
candidates.push({ id });
|
|
358105
|
+
}
|
|
358106
|
+
}
|
|
358107
|
+
return { candidates, defaultRunnerId: fromEnvironment ?? storedDefault };
|
|
358108
|
+
}
|
|
358109
|
+
async function probeRunner(ctx, runner) {
|
|
358110
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.get, { id: runner.id }, runnerCallOptions);
|
|
358111
|
+
if (!result.ok)
|
|
358112
|
+
return { ...failureFields(result), ok: false };
|
|
358113
|
+
return { ok: true, runner, running: result.value.running };
|
|
358114
|
+
}
|
|
358115
|
+
async function handleRunnerList(ctx, deps) {
|
|
358116
|
+
const { candidates, defaultRunnerId } = await readCandidates(deps);
|
|
358117
|
+
const running = [];
|
|
358118
|
+
const gone = [];
|
|
358119
|
+
for await (const probe of batchMap(candidates, (runner) => probeRunner(ctx, runner), probeBatchSize)) {
|
|
358120
|
+
if (!probe.ok) {
|
|
358121
|
+
return { ...failureFields(probe), exitCode: exitCodes.network };
|
|
358122
|
+
}
|
|
358123
|
+
if (probe.running)
|
|
358124
|
+
running.push(probe.runner);
|
|
358125
|
+
else
|
|
358126
|
+
gone.push(probe.runner.id);
|
|
358127
|
+
}
|
|
358128
|
+
await deps.store.dropRunners(gone).catch(() => {
|
|
358129
|
+
return;
|
|
358130
|
+
});
|
|
358131
|
+
const items = [
|
|
358132
|
+
...running.filter((runner) => runner.id === defaultRunnerId),
|
|
358133
|
+
...running.filter((runner) => runner.id !== defaultRunnerId)
|
|
358134
|
+
].map((runner) => ({
|
|
358135
|
+
id: runner.id,
|
|
358136
|
+
isDefault: runner.id === defaultRunnerId,
|
|
358137
|
+
runnerName: runner.runnerName
|
|
358138
|
+
}));
|
|
358139
|
+
if (ctx.ui.mode === "json") {
|
|
358140
|
+
ctx.ui.json(items);
|
|
358141
|
+
return;
|
|
358142
|
+
}
|
|
358143
|
+
if (items.length === 0) {
|
|
358144
|
+
ctx.ui.info(interactiveRunnerMessages.noRunners);
|
|
358145
|
+
return;
|
|
358146
|
+
}
|
|
358147
|
+
if (ctx.ui.mode === "agent") {
|
|
358148
|
+
ctx.ui.write(renderTable({ boldHeader: false, columns: columns2, rows: items }));
|
|
358149
|
+
return;
|
|
358150
|
+
}
|
|
358151
|
+
ctx.ui.gap();
|
|
358152
|
+
ctx.ui.intro(interactiveRunnerMessages.title);
|
|
358153
|
+
ctx.ui.write(renderTable({ boldHeader: true, columns: columns2, rows: items }));
|
|
358154
|
+
ctx.ui.outro(interactiveRunnerMessages.runnerCount(items.length));
|
|
358155
|
+
}
|
|
358156
|
+
|
|
357798
358157
|
// src/domains/interactiveRunner/stopRun.ts
|
|
357799
358158
|
async function handleRunnerStopRun(ctx, options, deps) {
|
|
357800
358159
|
const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
|
|
@@ -357828,11 +358187,7 @@ async function handleRunnerTerminate(ctx, options, deps) {
|
|
|
357828
358187
|
if (!result.ok) {
|
|
357829
358188
|
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
357830
358189
|
}
|
|
357831
|
-
await deps.store.
|
|
357832
|
-
if (storedDefault === resolved.runnerId) {
|
|
357833
|
-
await deps.store.clearDefaultRunnerId();
|
|
357834
|
-
}
|
|
357835
|
-
}).catch(() => {
|
|
358190
|
+
await deps.store.forgetRunner(resolved.runnerId).catch(() => {
|
|
357836
358191
|
ctx.ui.warn(interactiveRunnerMessages.defaultNotForgotten(resolved.runnerId));
|
|
357837
358192
|
});
|
|
357838
358193
|
ctx.ui.output(result.value, result.value.wasRunning ? interactiveRunnerMessages.terminated(result.value.id) : interactiveRunnerMessages.wasNotRunning(result.value.id));
|
|
@@ -357845,12 +358200,17 @@ Examples:
|
|
|
357845
358200
|
$ qawolf runner launch
|
|
357846
358201
|
$ qawolf runner launch --name android
|
|
357847
358202
|
$ qawolf runner launch --id ci`;
|
|
358203
|
+
var listExamples2 = `
|
|
358204
|
+
Examples:
|
|
358205
|
+
$ qawolf runner list
|
|
358206
|
+
$ qawolf runner list --json`;
|
|
357848
358207
|
var keepaliveExamples = `
|
|
357849
358208
|
Examples:
|
|
357850
358209
|
$ qawolf runner keepalive
|
|
357851
358210
|
$ qawolf runner keepalive --runner ci`;
|
|
357852
358211
|
function registerRunnerLifecycleCommands(runner, signals) {
|
|
357853
358212
|
declareCommandKind(runner.command("launch"), "write").description("Launch an interactive runner and make it this directory's default").option("--id <id>", "Id to launch under. Relaunching an id attaches to that runner").option("--name <family>", "Runner family to run, e.g. playwright").addHelpText("after", launchExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerLaunch(ctx, { id: opts.id, name: opts.name }, runnerDeps(ctx)))(opts, command));
|
|
358213
|
+
declareCommandKind(runner.command("list"), "read").description("List the runners this directory holds that are still running").addHelpText("after", listExamples2).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerList(ctx, runnerDeps(ctx)))(opts, command));
|
|
357854
358214
|
declareCommandKind(runner.command("terminate"), "write").description("End an interactive runner, and the pod it runs on with it").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerTerminate(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
357855
358215
|
declareCommandKind(runner.command("stop-run"), "write").description("Stop what a runner is currently executing, leaving the runner up").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerStopRun(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
357856
358216
|
declareCommandKind(runner.command("keepalive"), "read").description("Reset a runner's inactivity clock, for a caller that pauses between actions").option("--runner <id>", runnerFlagDescription).addHelpText("after", keepaliveExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerKeepalive(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
@@ -358648,4 +359008,4 @@ createProgram({ signals }).parseAsync().catch(() => {
|
|
|
358648
359008
|
process.exitCode = 1;
|
|
358649
359009
|
}).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
358650
359010
|
|
|
358651
|
-
//# debugId=
|
|
359011
|
+
//# debugId=C46D821567854FC564756E2164756E21
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qawolf/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@clack/prompts": "1.5.1",
|
|
61
61
|
"@napi-rs/keyring": "1.3.0",
|
|
62
62
|
"@oxc-node/core": "0.1.0",
|
|
63
|
-
"@qawolf/api-contracts": "0.
|
|
63
|
+
"@qawolf/api-contracts": "0.35.0",
|
|
64
64
|
"@qawolf/emails": "1.1.1",
|
|
65
65
|
"@qawolf/flow-targets": "1.0.0",
|
|
66
66
|
"@qawolf/flows": "0.1.4",
|
|
@@ -105,6 +105,17 @@ Inspect `git status` before publishing. Stage and commit only files changed for
|
|
|
105
105
|
the current task, preserve unrelated worktree changes, then push the task's
|
|
106
106
|
current branch.
|
|
107
107
|
|
|
108
|
+
The platform reads flows from the environment's flow code branch, not from your
|
|
109
|
+
task branch. `qawolf --json environment get` reports it as `flowCodeBranch`
|
|
110
|
+
(`name`, `syncStatus`, `lastSyncedCommitHash`). A flow has a platform page, an
|
|
111
|
+
id, and a place in `qawolf flows list --remote` only once it is on that branch
|
|
112
|
+
and the environment has reconciled the commit. To link or run a new flow on the
|
|
113
|
+
platform: integrate the flow code branch into your work, push the flow to that
|
|
114
|
+
branch, poll `environment get` until `lastSyncedCommitHash` includes your
|
|
115
|
+
commit, then read the flow's `flowId` and `url` from
|
|
116
|
+
`qawolf --json flows list --remote --env <environment> --include-drafts`. Send
|
|
117
|
+
that `url`; never guess a route and never send a repository link in its place.
|
|
118
|
+
|
|
108
119
|
## Commands
|
|
109
120
|
|
|
110
121
|
<!-- commands-table:start — generated by `bun run generate`, do not edit -->
|
|
@@ -144,16 +155,21 @@ current branch.
|
|
|
144
155
|
| `qawolf run find` | read | List an environment's recent runs, newest first. |
|
|
145
156
|
| `qawolf run get` | read | Get a run's status, per-flow results, and links. |
|
|
146
157
|
| `qawolf run reattempt` | write | Request new attempts for a run's flows, in the same run. A flow is eligible once its result is failed or canceled and QA Wolf's automatic retries have finished. A fully investigated run no longer accepts reattempts. Attempts run with the latest flow code. Poll run.get for results. |
|
|
147
|
-
| `qawolf runner act` | write | Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin |
|
|
158
|
+
| `qawolf runner act` | write | Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin. On a mobile runner only click (button left), drag and type have a touchscreen equivalent; the rest answer action-not-supported-on-mobile |
|
|
148
159
|
| `qawolf runner events` | read | Print a runner's journal, one entry per line. QA Wolf writes console, recorder, run-events, run-logs, run-status |
|
|
149
160
|
| `qawolf runner exec` | write | Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin |
|
|
150
161
|
| `qawolf runner highlight-selector` | write | Highlight what a selector matches on a runner's live page, so the next screenshot shows it. Omit the selector to clear the highlight |
|
|
151
162
|
| `qawolf runner import-package` | write | Install a package into a runner's live run, so a snippet or a selection can import it |
|
|
163
|
+
| `qawolf runner inspect contexts` | read | List the WebView contexts available, and which is current |
|
|
152
164
|
| `qawolf runner inspect element-html` | read | Print the HTML of the first element a selector matches |
|
|
165
|
+
| `qawolf runner inspect elements` | read | Find elements at a screen point, or elements carrying some text |
|
|
153
166
|
| `qawolf runner inspect page-html` | read | Print the page's HTML, simplified for a model to read |
|
|
167
|
+
| `qawolf runner inspect page-source` | read | Print the current context's page source, as a tree |
|
|
168
|
+
| `qawolf runner inspect session` | read | Print the Appium session's status: ready, or why not |
|
|
154
169
|
| `qawolf runner inspect variable` | read | Print a top-level variable's value from the running workflow |
|
|
155
170
|
| `qawolf runner keepalive` | read | Reset a runner's inactivity clock, for a caller that pauses between actions |
|
|
156
171
|
| `qawolf runner launch` | write | Launch an interactive runner and make it this directory's default |
|
|
172
|
+
| `qawolf runner list` | read | List the runners this directory holds that are still running |
|
|
157
173
|
| `qawolf runner promote-snapshot` | write | Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it |
|
|
158
174
|
| `qawolf runner run` | write | Run a flow on an interactive runner, shipping the flow and what it imports |
|
|
159
175
|
| `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file |
|
|
@@ -41,6 +41,41 @@ No `read` command ever launches a runner. `screenshot`, `events` and `keepalive`
|
|
|
41
41
|
tell you there is no runner rather than quietly billing one, and so does
|
|
42
42
|
`terminate`, since starting a pod in order to end it would be absurd.
|
|
43
43
|
|
|
44
|
+
`--name` also chooses between a browser and a mobile device:
|
|
45
|
+
`qawolf runner launch --name android` or `--name ios` starts an Appium session
|
|
46
|
+
instead of a browser. A command built for the other kind answers a
|
|
47
|
+
`failureReason` naming the mismatch rather than doing something approximate —
|
|
48
|
+
`runner-is-not-mobile` from `inspect session`/`contexts`/`page-source`/`elements`
|
|
49
|
+
on a browser runner, `runner-is-not-a-browser` from `inspect element-html`/
|
|
50
|
+
`page-html` on a mobile one — so launch the right family up front rather than
|
|
51
|
+
discovering it from a refusal mid-session.
|
|
52
|
+
|
|
53
|
+
## Knowing what you are holding
|
|
54
|
+
|
|
55
|
+
`qawolf runner list` names the runners this directory has launched that are
|
|
56
|
+
still running, and marks the one a command with no `--runner` would reach:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
id family default
|
|
60
|
+
tester-abc-main playwright yes
|
|
61
|
+
tester-abc-checkout playwright
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Every runner is looked up before it is listed, so a runner that idled out is
|
|
65
|
+
absent rather than reported. The lookup neither starts a runner nor resets an
|
|
66
|
+
inactivity clock, which is what separates `list` from `keepalive`: listing tells
|
|
67
|
+
you what is there and changes nothing, and holding a runner open is still
|
|
68
|
+
`keepalive`'s job. A lookup that cannot be answered fails the command rather
|
|
69
|
+
than returning a shorter list, because a short list reads as the whole truth.
|
|
70
|
+
|
|
71
|
+
Nothing is billed by listing, but everything in the list is billing. Terminate
|
|
72
|
+
what you are done with.
|
|
73
|
+
|
|
74
|
+
The list includes the runner named by `QAWOLF_RUNNER_ID` even though this
|
|
75
|
+
directory did not launch it, so a harness handed a runner sees it alongside the
|
|
76
|
+
ones it started itself. Use the `id` column with `--runner` to address any of
|
|
77
|
+
them; addressing one does not make it the default.
|
|
78
|
+
|
|
44
79
|
## The order that matters
|
|
45
80
|
|
|
46
81
|
A freshly launched runner has no screen. The virtual desktop starts with the
|
|
@@ -112,6 +147,14 @@ taken effect. On a `4` from `act`, take a screenshot before repeating a click.
|
|
|
112
147
|
looks the same from outside, so treat a `4` from a snippet that changes something
|
|
113
148
|
as "may have run" rather than "did not run".
|
|
114
149
|
|
|
150
|
+
A mobile runner has a touchscreen, not a mouse, so only three of the eight
|
|
151
|
+
actions have a touchscreen equivalent and go through: `click` with
|
|
152
|
+
`button: "left"` taps, `drag` swipes, and `type` types into whatever the last
|
|
153
|
+
tap focused. The rest — `double_click`, `scroll`, `move`, `keypress`,
|
|
154
|
+
`navigate` — answer `action-not-supported-on-mobile` rather than doing
|
|
155
|
+
something approximate. `navigate` is the one to watch for, since it works on a
|
|
156
|
+
browser runner without a run first but has no meaning on mobile at all.
|
|
157
|
+
|
|
115
158
|
## The recorder: what you cannot get from pixels
|
|
116
159
|
|
|
117
160
|
`qawolf runner events recorder` is the capability that has no equivalent in a
|
|
@@ -159,6 +202,52 @@ Use `inspect` before reaching for `exec`. Reading a value through a snippet
|
|
|
159
202
|
means printing it and then fishing it back out of the `console` stream, which is
|
|
160
203
|
two calls and a marker; `inspect variable` is one call and the value.
|
|
161
204
|
|
|
205
|
+
## Reading a mobile screen: `inspect session`/`contexts`/`page-source`/`elements`
|
|
206
|
+
|
|
207
|
+
`element-html`, `page-html` and `variable` are a browser's shapes. A mobile
|
|
208
|
+
runner answers four different ones instead, one subcommand per question:
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
qawolf runner inspect session
|
|
212
|
+
qawolf runner inspect contexts
|
|
213
|
+
qawolf runner inspect page-source
|
|
214
|
+
qawolf runner inspect page-source --context WEBVIEW_1 # a specific context, not the current one
|
|
215
|
+
qawolf runner inspect elements --by point --x 240 --y 480
|
|
216
|
+
qawolf runner inspect elements --by text --text "Sign in" --partial
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`session` prints one summary line — ready, or why not — because that line is
|
|
220
|
+
the whole answer. `contexts`, `page-source` and `elements` instead print their
|
|
221
|
+
answer as JSON on stdout, on its own like `inspect element-html`, so you can
|
|
222
|
+
redirect or pipe it (`| jq .current`, `| jq .matches`, `| jq .pageSource`)
|
|
223
|
+
rather than getting a count with no way to see what was actually found.
|
|
224
|
+
|
|
225
|
+
`session` is also the one subcommand that never answers `screen-needs-a-run`:
|
|
226
|
+
readiness is the question it exists to answer, so it reports one of `ready`
|
|
227
|
+
(with the platform, device and session id), `unreachable`, `ambiguous` (more
|
|
228
|
+
than one session is somehow live) or `no-session`, rather than refusing until a
|
|
229
|
+
run has happened. The other three need a live session first and share the same
|
|
230
|
+
readiness contract `act` and `screenshot` use on a browser runner:
|
|
231
|
+
`screen-needs-a-run` exits `2` and means no Appium session has started on this
|
|
232
|
+
runner yet — run a flow that opens one, then inspect again; `screen-not-ready`
|
|
233
|
+
exits `4` and means the session exists but did not answer this instant, or more
|
|
234
|
+
than one is somehow live — retry once, and relaunch the runner if it persists.
|
|
235
|
+
|
|
236
|
+
`elements` takes one of two ways to search, chosen with `--by`: `point` needs
|
|
237
|
+
whole-pixel `--x`/`--y` on the device's own screen, the same coordinates a
|
|
238
|
+
screenshot is measured in; `text` needs `--text` and matches it exactly unless
|
|
239
|
+
`--partial` is passed. The two do not mix — `--by point` with `--text` set, or
|
|
240
|
+
`--by text` with `--x`/`--y` set, is refused before a runner is addressed
|
|
241
|
+
rather than silently searching by the one it picked, since the schema itself
|
|
242
|
+
just strips whichever field the chosen `by` does not define. `--context` on
|
|
243
|
+
`page-source` or `elements` reads a context other than the current one —
|
|
244
|
+
useful once `contexts` has told you which are available.
|
|
245
|
+
|
|
246
|
+
A browser runner answers `runner-is-not-mobile` to all four, exit `2`, since
|
|
247
|
+
retrying never helps: launch with `--name android` or `--name ios` instead.
|
|
248
|
+
`runner-unreachable` exits `4` and is worth retrying, same as everywhere else
|
|
249
|
+
on this surface.
|
|
250
|
+
|
|
162
251
|
## Checking a selector: `highlight-selector`
|
|
163
252
|
|
|
164
253
|
`inspect element-html` tells you what a selector matched. `highlight-selector`
|