@qawolf/cli 1.17.0 → 1.18.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 +265 -80
- package/package.json +2 -2
- package/skills/qawolf-cli/SKILL.md +15 -1
- package/skills/qawolf-cli/references/runner.md +38 -0
package/dist/cli.js
CHANGED
|
@@ -326161,6 +326161,13 @@ var lifecycleMessages = {
|
|
|
326161
326161
|
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
326162
|
};
|
|
326163
326163
|
|
|
326164
|
+
// src/core/messages/interactiveRunner/list.ts
|
|
326165
|
+
var listMessages = {
|
|
326166
|
+
noRunners: "This directory has no runner running. Launch one with qawolf runner launch, or pass --runner to address a runner started elsewhere.",
|
|
326167
|
+
runnerCount: (count) => count === 1 ? "1 runner" : `${String(count)} runners`,
|
|
326168
|
+
title: "Runners"
|
|
326169
|
+
};
|
|
326170
|
+
|
|
326164
326171
|
// src/core/messages/interactiveRunner/run.ts
|
|
326165
326172
|
var runMessages = {
|
|
326166
326173
|
envFileUnparseable: (reason) => `The --env-file could not be read. ${reason}`,
|
|
@@ -326198,6 +326205,7 @@ var runMessages = {
|
|
|
326198
326205
|
// src/core/messages/interactiveRunner/index.ts
|
|
326199
326206
|
var interactiveRunnerMessages = {
|
|
326200
326207
|
...lifecycleMessages,
|
|
326208
|
+
...listMessages,
|
|
326201
326209
|
...runMessages,
|
|
326202
326210
|
...interactMessages
|
|
326203
326211
|
};
|
|
@@ -343242,6 +343250,25 @@ var makeEvaluateSnippetOnRunnerContract = () => {
|
|
|
343242
343250
|
};
|
|
343243
343251
|
};
|
|
343244
343252
|
|
|
343253
|
+
// node_modules/@qawolf/api-contracts/dist/v1/runner/get.js
|
|
343254
|
+
var makeGetRunnerContract = () => {
|
|
343255
|
+
const input = exports_external.object({
|
|
343256
|
+
id: runnerIdSchema.describe("Id of the runner to look up.")
|
|
343257
|
+
});
|
|
343258
|
+
const output = exports_external.object({
|
|
343259
|
+
id: runnerIdSchema.describe("Id of the runner the call addressed."),
|
|
343260
|
+
outcome: exports_external.literal("success"),
|
|
343261
|
+
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.")
|
|
343262
|
+
});
|
|
343263
|
+
return {
|
|
343264
|
+
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.",
|
|
343265
|
+
input,
|
|
343266
|
+
kind: "read",
|
|
343267
|
+
name: "runner.get",
|
|
343268
|
+
output
|
|
343269
|
+
};
|
|
343270
|
+
};
|
|
343271
|
+
|
|
343245
343272
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/highlightSelector.js
|
|
343246
343273
|
var maxHighlightSelectorLength = 2000;
|
|
343247
343274
|
var makeHighlightSelectorOnRunnerContract = () => {
|
|
@@ -343563,6 +343590,7 @@ var makeContractsV1 = (ids) => {
|
|
|
343563
343590
|
},
|
|
343564
343591
|
runner: {
|
|
343565
343592
|
evaluateSnippet: makeEvaluateSnippetOnRunnerContract(),
|
|
343593
|
+
get: makeGetRunnerContract(),
|
|
343566
343594
|
highlightSelector: makeHighlightSelectorOnRunnerContract(),
|
|
343567
343595
|
importPackage: makeImportPackageOnRunnerContract(),
|
|
343568
343596
|
inspect: makeInspectOnRunnerContract(),
|
|
@@ -346098,7 +346126,7 @@ function startUpdateCheck(deps) {
|
|
|
346098
346126
|
// package.json
|
|
346099
346127
|
var package_default = {
|
|
346100
346128
|
name: "@qawolf/cli",
|
|
346101
|
-
version: "1.
|
|
346129
|
+
version: "1.18.0",
|
|
346102
346130
|
description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
346103
346131
|
keywords: [
|
|
346104
346132
|
"automation",
|
|
@@ -346158,7 +346186,7 @@ var package_default = {
|
|
|
346158
346186
|
"@clack/prompts": "1.5.1",
|
|
346159
346187
|
"@napi-rs/keyring": "1.3.0",
|
|
346160
346188
|
"@oxc-node/core": "0.1.0",
|
|
346161
|
-
"@qawolf/api-contracts": "0.
|
|
346189
|
+
"@qawolf/api-contracts": "0.35.0",
|
|
346162
346190
|
"@qawolf/emails": "1.1.1",
|
|
346163
346191
|
"@qawolf/flow-targets": "1.0.0",
|
|
346164
346192
|
"@qawolf/flows": "0.1.4",
|
|
@@ -348590,28 +348618,35 @@ Examples:
|
|
|
348590
348618
|
// src/domains/flows/list.ts
|
|
348591
348619
|
import path2 from "node:path";
|
|
348592
348620
|
|
|
348593
|
-
// src/
|
|
348594
|
-
var columns = ["name", "target", "file"];
|
|
348621
|
+
// src/core/renderTable.ts
|
|
348595
348622
|
var ansiBold = "\x1B[1m";
|
|
348596
348623
|
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(`
|
|
348624
|
+
function renderTable(options) {
|
|
348625
|
+
const { boldHeader, columns, rows } = options;
|
|
348626
|
+
const measured = columns.map((column) => ({
|
|
348627
|
+
column,
|
|
348628
|
+
width: Math.max(column.header.length, ...rows.map((row) => column.value(row).length))
|
|
348629
|
+
}));
|
|
348630
|
+
const renderRow = (cell) => measured.map(({ column, width }) => cell(column).padEnd(width)).join(" ").trimEnd();
|
|
348631
|
+
const header = renderRow((column) => column.header);
|
|
348632
|
+
return [
|
|
348633
|
+
boldHeader ? `${ansiBold}${header}${ansiReset}` : header,
|
|
348634
|
+
...rows.map((row) => renderRow((column) => column.value(row)))
|
|
348635
|
+
].join(`
|
|
348611
348636
|
`) + `
|
|
348612
348637
|
`;
|
|
348613
348638
|
}
|
|
348614
348639
|
|
|
348640
|
+
// src/domains/flows/renderListTable.ts
|
|
348641
|
+
var columns = [
|
|
348642
|
+
{ header: "name", value: (row) => row.name },
|
|
348643
|
+
{ header: "target", value: (row) => row.target },
|
|
348644
|
+
{ header: "file", value: (row) => row.file }
|
|
348645
|
+
];
|
|
348646
|
+
function renderListTable(rows, boldHeader) {
|
|
348647
|
+
return renderTable({ boldHeader, columns, rows });
|
|
348648
|
+
}
|
|
348649
|
+
|
|
348615
348650
|
// src/domains/flows/list.ts
|
|
348616
348651
|
async function flowsList(ctx, pattern, deps) {
|
|
348617
348652
|
const patterns = pattern ? [pattern] : [];
|
|
@@ -348672,7 +348707,8 @@ async function flowsListRemote(ctx, pattern, options) {
|
|
|
348672
348707
|
file: f2.path,
|
|
348673
348708
|
name: f2.name,
|
|
348674
348709
|
tags: f2.tags,
|
|
348675
|
-
target: typeof f2.executionTarget === "string" ? f2.executionTarget : JSON.stringify(f2.executionTarget)
|
|
348710
|
+
target: typeof f2.executionTarget === "string" ? f2.executionTarget : JSON.stringify(f2.executionTarget),
|
|
348711
|
+
url: f2.url
|
|
348676
348712
|
}));
|
|
348677
348713
|
if (ctx.ui.mode === "json") {
|
|
348678
348714
|
ctx.ui.json(items);
|
|
@@ -356592,6 +356628,7 @@ async function handlePublicApiCommand(ctx, spec, options) {
|
|
|
356592
356628
|
var skippedContractNames = new Set([
|
|
356593
356629
|
"flow.list",
|
|
356594
356630
|
"runner.evaluateSnippet",
|
|
356631
|
+
"runner.get",
|
|
356595
356632
|
"runner.highlightSelector",
|
|
356596
356633
|
"runner.importPackage",
|
|
356597
356634
|
"runner.inspect",
|
|
@@ -356660,21 +356697,7 @@ function registerPublicApiCommands(program2, signals, options = {}) {
|
|
|
356660
356697
|
// src/domains/interactiveRunner/runnerCallOptions.ts
|
|
356661
356698
|
var runnerCallOptions = { timeoutMs: 90000 };
|
|
356662
356699
|
|
|
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
|
|
356700
|
+
// src/domains/interactiveRunner/launchAndRemember.ts
|
|
356678
356701
|
async function launchRunner(ctx, options) {
|
|
356679
356702
|
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.launch, {
|
|
356680
356703
|
id: options.id,
|
|
@@ -356694,38 +356717,50 @@ async function launchAndRemember(ctx, options, deps) {
|
|
|
356694
356717
|
const priorDefault = await deps.store.readDefaultRunnerId().catch(() => {
|
|
356695
356718
|
return;
|
|
356696
356719
|
});
|
|
356697
|
-
const remembered = await deps.store.
|
|
356720
|
+
const remembered = await deps.store.rememberLaunch({ id: options.id, runnerName: options.runnerName }).then(() => true).catch(() => {
|
|
356698
356721
|
ctx.ui.warn(interactiveRunnerMessages.defaultNotRemembered(options.id));
|
|
356699
356722
|
return false;
|
|
356700
356723
|
});
|
|
356701
356724
|
const launched = await launchRunner(ctx, options);
|
|
356702
|
-
if (launched.ok)
|
|
356725
|
+
if (launched.ok) {
|
|
356726
|
+
if (remembered) {
|
|
356727
|
+
await deps.store.rememberLaunch({
|
|
356728
|
+
id: options.id,
|
|
356729
|
+
runnerName: launched.value.runnerName
|
|
356730
|
+
}).catch(() => {
|
|
356731
|
+
return;
|
|
356732
|
+
});
|
|
356733
|
+
}
|
|
356703
356734
|
return launched;
|
|
356735
|
+
}
|
|
356704
356736
|
if (remembered && !launched.mayHaveArrived) {
|
|
356705
|
-
await
|
|
356737
|
+
await deps.store.forgetRunner(options.id).catch(() => {
|
|
356706
356738
|
return;
|
|
356707
356739
|
});
|
|
356740
|
+
if (priorDefault !== undefined) {
|
|
356741
|
+
await deps.store.writeDefaultRunnerId(priorDefault).catch(() => {
|
|
356742
|
+
return;
|
|
356743
|
+
});
|
|
356744
|
+
}
|
|
356708
356745
|
}
|
|
356709
356746
|
return {
|
|
356710
356747
|
...launched,
|
|
356711
356748
|
error: launched.mayHaveArrived ? interactiveRunnerMessages.launchLost(options.id, launched.error) : interactiveRunnerMessages.launchFailed(options.id, launched.error)
|
|
356712
356749
|
};
|
|
356713
356750
|
}
|
|
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;
|
|
356751
|
+
|
|
356752
|
+
// src/domains/interactiveRunner/runnerIds.ts
|
|
356753
|
+
function parseRunnerId(id) {
|
|
356754
|
+
const parsed = runnerIdSchema.safeParse(id);
|
|
356755
|
+
if (!parsed.success)
|
|
356756
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
356757
|
+
return { id: parsed.data, ok: true };
|
|
356758
|
+
}
|
|
356759
|
+
function parseRunnerName(runnerName) {
|
|
356760
|
+
const parsed = runnerNameSchema.safeParse(runnerName);
|
|
356761
|
+
if (!parsed.success)
|
|
356762
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
356763
|
+
return { ok: true, runnerName: parsed.data };
|
|
356729
356764
|
}
|
|
356730
356765
|
|
|
356731
356766
|
// src/domains/interactiveRunner/resolveRunner.ts
|
|
@@ -357077,7 +357112,14 @@ function parseJson(text3) {
|
|
|
357077
357112
|
// src/shell/interactiveRunner/runnerStore.ts
|
|
357078
357113
|
import { join as join47 } from "node:path";
|
|
357079
357114
|
var storeFileName = "runner.json";
|
|
357080
|
-
var
|
|
357115
|
+
var runnersDirName = "runners";
|
|
357116
|
+
var storedRunnerSchema = exports_external.object({
|
|
357117
|
+
id: exports_external.string(),
|
|
357118
|
+
runnerName: exports_external.string().optional()
|
|
357119
|
+
});
|
|
357120
|
+
var storeSchema = exports_external.object({
|
|
357121
|
+
defaultRunnerId: exports_external.string().optional()
|
|
357122
|
+
});
|
|
357081
357123
|
function parseJson2(text3) {
|
|
357082
357124
|
try {
|
|
357083
357125
|
return JSON.parse(text3);
|
|
@@ -357085,30 +357127,81 @@ function parseJson2(text3) {
|
|
|
357085
357127
|
return;
|
|
357086
357128
|
}
|
|
357087
357129
|
}
|
|
357130
|
+
var pendingWrites = 0;
|
|
357088
357131
|
function makeRunnerStore(options) {
|
|
357089
357132
|
const directory = join47(options.cwd, qawolfDir);
|
|
357090
357133
|
const path9 = join47(directory, storeFileName);
|
|
357091
|
-
|
|
357092
|
-
const nextPendingPath = () => `${
|
|
357134
|
+
const runnersDir = join47(directory, runnersDirName);
|
|
357135
|
+
const nextPendingPath = (target) => `${target}.${process.pid}.${++pendingWrites}.tmp`;
|
|
357136
|
+
const runnerPath = (runnerId) => join47(runnersDir, `${encodeURIComponent(runnerId)}.json`);
|
|
357137
|
+
const writeAtomically = async (target, contents2) => {
|
|
357138
|
+
const pendingPath = nextPendingPath(target);
|
|
357139
|
+
await options.fs.writeFile(pendingPath, `${JSON.stringify(contents2, undefined, 2)}
|
|
357140
|
+
`);
|
|
357141
|
+
await options.fs.rename(pendingPath, target);
|
|
357142
|
+
};
|
|
357143
|
+
const readDefaultRunnerId = async () => {
|
|
357144
|
+
const contents2 = await options.fs.readFile(path9).catch(() => {
|
|
357145
|
+
return;
|
|
357146
|
+
});
|
|
357147
|
+
if (contents2 === undefined)
|
|
357148
|
+
return;
|
|
357149
|
+
const parsed = storeSchema.safeParse(parseJson2(contents2));
|
|
357150
|
+
return parsed.success ? parsed.data.defaultRunnerId : undefined;
|
|
357151
|
+
};
|
|
357152
|
+
const writeDefaultRunnerId = async (runnerId) => {
|
|
357153
|
+
await options.fs.mkdir(directory, { recursive: true });
|
|
357154
|
+
await writeAtomically(path9, { defaultRunnerId: runnerId });
|
|
357155
|
+
};
|
|
357156
|
+
const readRunnerFileNames = async () => {
|
|
357157
|
+
try {
|
|
357158
|
+
const names = await options.fs.readdir(runnersDir);
|
|
357159
|
+
return names.filter((name) => name.endsWith(".json"));
|
|
357160
|
+
} catch {
|
|
357161
|
+
return [];
|
|
357162
|
+
}
|
|
357163
|
+
};
|
|
357093
357164
|
return {
|
|
357094
|
-
async
|
|
357095
|
-
await options.fs.rm(
|
|
357165
|
+
async forgetRunner(runnerId) {
|
|
357166
|
+
await options.fs.rm(runnerPath(runnerId), { force: true });
|
|
357167
|
+
if (await readDefaultRunnerId() === runnerId)
|
|
357168
|
+
await writeDefaultRunnerId(undefined);
|
|
357096
357169
|
},
|
|
357097
|
-
|
|
357098
|
-
|
|
357099
|
-
|
|
357100
|
-
|
|
357101
|
-
|
|
357102
|
-
|
|
357103
|
-
|
|
357104
|
-
|
|
357170
|
+
readDefaultRunnerId,
|
|
357171
|
+
async readRunners() {
|
|
357172
|
+
const runners = await Promise.all((await readRunnerFileNames()).map(async (name) => {
|
|
357173
|
+
const contents2 = await options.fs.readFile(join47(runnersDir, name)).catch(() => {
|
|
357174
|
+
return;
|
|
357175
|
+
});
|
|
357176
|
+
if (contents2 === undefined)
|
|
357177
|
+
return;
|
|
357178
|
+
const parsed = storedRunnerSchema.safeParse(parseJson2(contents2));
|
|
357179
|
+
return parsed.success ? parsed.data : undefined;
|
|
357180
|
+
}));
|
|
357181
|
+
return runners.filter((runner) => !!runner).sort((left, right) => left.id.localeCompare(right.id));
|
|
357182
|
+
},
|
|
357183
|
+
async rememberLaunch(runner) {
|
|
357184
|
+
await options.fs.mkdir(runnersDir, { recursive: true });
|
|
357185
|
+
await writeAtomically(runnerPath(runner.id), runner);
|
|
357186
|
+
await writeDefaultRunnerId(runner.id);
|
|
357187
|
+
},
|
|
357188
|
+
async dropRunners(runnerIds) {
|
|
357189
|
+
await Promise.all(runnerIds.map((runnerId) => options.fs.rm(runnerPath(runnerId), { force: true })));
|
|
357190
|
+
const names = await readRunnerFileNames();
|
|
357191
|
+
await Promise.all(names.map(async (name) => {
|
|
357192
|
+
const runnerFile = join47(runnersDir, name);
|
|
357193
|
+
const contents2 = await options.fs.readFile(runnerFile).catch(() => {
|
|
357194
|
+
return;
|
|
357195
|
+
});
|
|
357196
|
+
if (contents2 === undefined)
|
|
357197
|
+
return;
|
|
357198
|
+
if (storedRunnerSchema.safeParse(parseJson2(contents2)).success)
|
|
357199
|
+
return;
|
|
357200
|
+
await options.fs.rm(runnerFile, { force: true });
|
|
357201
|
+
}));
|
|
357105
357202
|
},
|
|
357106
357203
|
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);
|
|
357204
|
+
await writeDefaultRunnerId(runnerId);
|
|
357112
357205
|
}
|
|
357113
357206
|
};
|
|
357114
357207
|
}
|
|
@@ -357795,6 +357888,92 @@ async function handleRunnerKeepalive(ctx, options, deps) {
|
|
|
357795
357888
|
return;
|
|
357796
357889
|
}
|
|
357797
357890
|
|
|
357891
|
+
// src/domains/interactiveRunner/launch.ts
|
|
357892
|
+
async function handleRunnerLaunch(ctx, options, deps) {
|
|
357893
|
+
const chosenId = options.id ?? deps.makeRunnerId();
|
|
357894
|
+
const id = parseRunnerId(chosenId);
|
|
357895
|
+
if (!id.ok)
|
|
357896
|
+
return { error: id.error, exitCode: exitCodes.invalidArgs };
|
|
357897
|
+
const runnerName = options.name === undefined ? undefined : parseRunnerName(options.name);
|
|
357898
|
+
if (runnerName !== undefined && !runnerName.ok) {
|
|
357899
|
+
return { error: runnerName.error, exitCode: exitCodes.invalidArgs };
|
|
357900
|
+
}
|
|
357901
|
+
const launched = await launchAndRemember(ctx, { id: id.id, runnerName: runnerName?.runnerName }, deps);
|
|
357902
|
+
if (!launched.ok) {
|
|
357903
|
+
return { ...failureFields(launched), exitCode: launched.exitCode };
|
|
357904
|
+
}
|
|
357905
|
+
ctx.ui.output(launched.value, launched.value.alreadyRunning ? interactiveRunnerMessages.alreadyRunning(launched.value.id) : interactiveRunnerMessages.launched(launched.value.id));
|
|
357906
|
+
return;
|
|
357907
|
+
}
|
|
357908
|
+
|
|
357909
|
+
// src/domains/interactiveRunner/list.ts
|
|
357910
|
+
var columns2 = [
|
|
357911
|
+
{ header: "id", value: (row) => row.id },
|
|
357912
|
+
{ header: "family", value: (row) => row.runnerName ?? "" },
|
|
357913
|
+
{ header: "default", value: (row) => row.isDefault ? "yes" : "" }
|
|
357914
|
+
];
|
|
357915
|
+
var probeBatchSize = 8;
|
|
357916
|
+
async function readCandidates(deps) {
|
|
357917
|
+
const held = await deps.store.readRunners();
|
|
357918
|
+
const storedDefault = await deps.store.readDefaultRunnerId();
|
|
357919
|
+
const environmentValue = deps.env[runnerIdEnvironmentVariable]?.trim();
|
|
357920
|
+
const fromEnvironment = environmentValue ? environmentValue : undefined;
|
|
357921
|
+
const candidates = [...held];
|
|
357922
|
+
for (const id of [storedDefault, fromEnvironment]) {
|
|
357923
|
+
if (id !== undefined && !candidates.some((runner) => runner.id === id)) {
|
|
357924
|
+
candidates.push({ id });
|
|
357925
|
+
}
|
|
357926
|
+
}
|
|
357927
|
+
return { candidates, defaultRunnerId: fromEnvironment ?? storedDefault };
|
|
357928
|
+
}
|
|
357929
|
+
async function probeRunner(ctx, runner) {
|
|
357930
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.get, { id: runner.id }, runnerCallOptions);
|
|
357931
|
+
if (!result.ok)
|
|
357932
|
+
return { ...failureFields(result), ok: false };
|
|
357933
|
+
return { ok: true, runner, running: result.value.running };
|
|
357934
|
+
}
|
|
357935
|
+
async function handleRunnerList(ctx, deps) {
|
|
357936
|
+
const { candidates, defaultRunnerId } = await readCandidates(deps);
|
|
357937
|
+
const running = [];
|
|
357938
|
+
const gone = [];
|
|
357939
|
+
for await (const probe of batchMap(candidates, (runner) => probeRunner(ctx, runner), probeBatchSize)) {
|
|
357940
|
+
if (!probe.ok) {
|
|
357941
|
+
return { ...failureFields(probe), exitCode: exitCodes.network };
|
|
357942
|
+
}
|
|
357943
|
+
if (probe.running)
|
|
357944
|
+
running.push(probe.runner);
|
|
357945
|
+
else
|
|
357946
|
+
gone.push(probe.runner.id);
|
|
357947
|
+
}
|
|
357948
|
+
await deps.store.dropRunners(gone).catch(() => {
|
|
357949
|
+
return;
|
|
357950
|
+
});
|
|
357951
|
+
const items = [
|
|
357952
|
+
...running.filter((runner) => runner.id === defaultRunnerId),
|
|
357953
|
+
...running.filter((runner) => runner.id !== defaultRunnerId)
|
|
357954
|
+
].map((runner) => ({
|
|
357955
|
+
id: runner.id,
|
|
357956
|
+
isDefault: runner.id === defaultRunnerId,
|
|
357957
|
+
runnerName: runner.runnerName
|
|
357958
|
+
}));
|
|
357959
|
+
if (ctx.ui.mode === "json") {
|
|
357960
|
+
ctx.ui.json(items);
|
|
357961
|
+
return;
|
|
357962
|
+
}
|
|
357963
|
+
if (items.length === 0) {
|
|
357964
|
+
ctx.ui.info(interactiveRunnerMessages.noRunners);
|
|
357965
|
+
return;
|
|
357966
|
+
}
|
|
357967
|
+
if (ctx.ui.mode === "agent") {
|
|
357968
|
+
ctx.ui.write(renderTable({ boldHeader: false, columns: columns2, rows: items }));
|
|
357969
|
+
return;
|
|
357970
|
+
}
|
|
357971
|
+
ctx.ui.gap();
|
|
357972
|
+
ctx.ui.intro(interactiveRunnerMessages.title);
|
|
357973
|
+
ctx.ui.write(renderTable({ boldHeader: true, columns: columns2, rows: items }));
|
|
357974
|
+
ctx.ui.outro(interactiveRunnerMessages.runnerCount(items.length));
|
|
357975
|
+
}
|
|
357976
|
+
|
|
357798
357977
|
// src/domains/interactiveRunner/stopRun.ts
|
|
357799
357978
|
async function handleRunnerStopRun(ctx, options, deps) {
|
|
357800
357979
|
const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
|
|
@@ -357828,11 +358007,7 @@ async function handleRunnerTerminate(ctx, options, deps) {
|
|
|
357828
358007
|
if (!result.ok) {
|
|
357829
358008
|
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
357830
358009
|
}
|
|
357831
|
-
await deps.store.
|
|
357832
|
-
if (storedDefault === resolved.runnerId) {
|
|
357833
|
-
await deps.store.clearDefaultRunnerId();
|
|
357834
|
-
}
|
|
357835
|
-
}).catch(() => {
|
|
358010
|
+
await deps.store.forgetRunner(resolved.runnerId).catch(() => {
|
|
357836
358011
|
ctx.ui.warn(interactiveRunnerMessages.defaultNotForgotten(resolved.runnerId));
|
|
357837
358012
|
});
|
|
357838
358013
|
ctx.ui.output(result.value, result.value.wasRunning ? interactiveRunnerMessages.terminated(result.value.id) : interactiveRunnerMessages.wasNotRunning(result.value.id));
|
|
@@ -357845,12 +358020,17 @@ Examples:
|
|
|
357845
358020
|
$ qawolf runner launch
|
|
357846
358021
|
$ qawolf runner launch --name android
|
|
357847
358022
|
$ qawolf runner launch --id ci`;
|
|
358023
|
+
var listExamples2 = `
|
|
358024
|
+
Examples:
|
|
358025
|
+
$ qawolf runner list
|
|
358026
|
+
$ qawolf runner list --json`;
|
|
357848
358027
|
var keepaliveExamples = `
|
|
357849
358028
|
Examples:
|
|
357850
358029
|
$ qawolf runner keepalive
|
|
357851
358030
|
$ qawolf runner keepalive --runner ci`;
|
|
357852
358031
|
function registerRunnerLifecycleCommands(runner, signals) {
|
|
357853
358032
|
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));
|
|
358033
|
+
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
358034
|
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
358035
|
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
358036
|
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));
|
|
@@ -358305,13 +358485,15 @@ var refused = (error51, exitCode) => ({
|
|
|
358305
358485
|
ok: false
|
|
358306
358486
|
});
|
|
358307
358487
|
async function prepareRun(options, deps) {
|
|
358308
|
-
const
|
|
358309
|
-
if (
|
|
358488
|
+
const explicitEnvId = options.envId?.trim();
|
|
358489
|
+
if (explicitEnvId !== undefined && options.envFile !== undefined) {
|
|
358310
358490
|
return refused(interactiveRunnerMessages.envIdWithEnvFile, exitCodes.invalidArgs);
|
|
358311
358491
|
}
|
|
358312
|
-
if (
|
|
358492
|
+
if (explicitEnvId === "") {
|
|
358313
358493
|
return refused(interactiveRunnerMessages.envIdBlank, exitCodes.invalidArgs);
|
|
358314
358494
|
}
|
|
358495
|
+
const fromEnvVar = deps.env["QAWOLF_ENVIRONMENT"]?.trim();
|
|
358496
|
+
const environmentId = explicitEnvId ?? (options.envFile === undefined && fromEnvVar ? fromEnvVar : undefined);
|
|
358315
358497
|
const linesFilePath = options.linesFile === undefined ? undefined : toCollectedPath(deps.cwd, options.linesFile);
|
|
358316
358498
|
const roots = linesFilePath === undefined || linesFilePath === options.entryPointPath ? [options.entryPointPath] : [options.entryPointPath, linesFilePath];
|
|
358317
358499
|
const collected = await collectRunFiles2(deps, roots);
|
|
@@ -358522,6 +358704,9 @@ async function handleRunnerRun(ctx, options, deps) {
|
|
|
358522
358704
|
if (!prepared.ok) {
|
|
358523
358705
|
return { error: prepared.error, exitCode: prepared.exitCode };
|
|
358524
358706
|
}
|
|
358707
|
+
if (options.envId === undefined && prepared.environmentId !== undefined) {
|
|
358708
|
+
ctx.ui.info(environmentsMessages.usingFromEnvVar(prepared.environmentId));
|
|
358709
|
+
}
|
|
358525
358710
|
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
358526
358711
|
if (resolved.type === "failed") {
|
|
358527
358712
|
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
@@ -358579,7 +358764,7 @@ Examples:
|
|
|
358579
358764
|
$ qawolf runner run flows/checkout.flow.ts --lines 4-9 --lines-file pages/login.ts
|
|
358580
358765
|
$ qawolf runner run flows/checkout.flow.ts --env-id staging`;
|
|
358581
358766
|
function registerRunCommand(runner, signals) {
|
|
358582
|
-
declareCommandKind(runner.command("run <flowFile>"), "write").description("Run a flow on an interactive runner, shipping the flow and what it imports").option("--follow", "Report the run's status until it settles: in progress, then passed or failed", false).option("--logs", "Stream every log line the run produces while following. Implies --follow", false).option("--run-events", "Stream the run's progress events as JSON lines while following. Implies --follow", false).option("--recorder-events", "Stream the browser actions the runner records as JSON lines while following, from an anchor taken just before submission: the recorder is runner-wide, not run-scoped. Implies --follow", false).option("--env-id <env>", "QA Wolf environment whose variables the run is given, by id or alias. QA Wolf reads and decrypts them itself, so they never leave the server and no size limit applies to them").option("--env-file <path>", "Dotenv file on this machine whose variables the run is given. Pass this or --env-id, not both").option("--lines <start-end>", "Run only these 1-indexed lines against the browser as it stands, instead of the whole flow from a fresh one").option("--lines-file <path>", "File the --lines range lives in. Defaults to <flowFile>; pass it only when the lines are in another file, such as a page object").option("--runner <id>", runnerFlagDescription).option("--timeout <seconds>", "Give up following after this long. Following keeps the runner alive, so a run that never settles would otherwise bill until the terminal closed", String(defaultFollowTimeoutSeconds)).addHelpText("after", runExamples2).action((flowFile, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRun(ctx, {
|
|
358767
|
+
declareCommandKind(runner.command("run <flowFile>"), "write").description("Run a flow on an interactive runner, shipping the flow and what it imports").option("--follow", "Report the run's status until it settles: in progress, then passed or failed", false).option("--logs", "Stream every log line the run produces while following. Implies --follow", false).option("--run-events", "Stream the run's progress events as JSON lines while following. Implies --follow", false).option("--recorder-events", "Stream the browser actions the runner records as JSON lines while following, from an anchor taken just before submission: the recorder is runner-wide, not run-scoped. Implies --follow", false).option("--env-id <env>", "QA Wolf environment whose variables the run is given, by id or alias. Defaults to QAWOLF_ENVIRONMENT. QA Wolf reads and decrypts them itself, so they never leave the server and no size limit applies to them").option("--env-file <path>", "Dotenv file on this machine whose variables the run is given. Pass this or --env-id, not both").option("--lines <start-end>", "Run only these 1-indexed lines against the browser as it stands, instead of the whole flow from a fresh one").option("--lines-file <path>", "File the --lines range lives in. Defaults to <flowFile>; pass it only when the lines are in another file, such as a page object").option("--runner <id>", runnerFlagDescription).option("--timeout <seconds>", "Give up following after this long. Following keeps the runner alive, so a run that never settles would otherwise bill until the terminal closed", String(defaultFollowTimeoutSeconds)).addHelpText("after", runExamples2).action((flowFile, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRun(ctx, {
|
|
358583
358768
|
entryPoint: flowFile,
|
|
358584
358769
|
envFile: opts.envFile,
|
|
358585
358770
|
envId: opts.envId,
|
|
@@ -358643,4 +358828,4 @@ createProgram({ signals }).parseAsync().catch(() => {
|
|
|
358643
358828
|
process.exitCode = 1;
|
|
358644
358829
|
}).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
358645
358830
|
|
|
358646
|
-
//# debugId=
|
|
358831
|
+
//# debugId=B6D9FB052BE12D6D64756E2164756E21
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qawolf/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.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",
|
|
@@ -38,7 +38,9 @@ is set; otherwise select an id or alias from
|
|
|
38
38
|
`qawolf --json environment find` and reuse it. Shell invocations may not share
|
|
39
39
|
exported variables, so pass the selected environment explicitly instead of
|
|
40
40
|
rediscovering it. Check the command's help for its environment flag: variable
|
|
41
|
-
commands use `--environment-id`; `qawolf flows run` uses `--env
|
|
41
|
+
commands use `--environment-id`; `qawolf flows run` uses `--env`; and
|
|
42
|
+
`qawolf runner run` uses `--env-id`, which falls back to `QAWOLF_ENVIRONMENT`
|
|
43
|
+
when neither it nor `--env-file` is passed.
|
|
42
44
|
|
|
43
45
|
If multiple environments are returned and the task context does not identify
|
|
44
46
|
the target, ask instead of guessing. Do not default to the newest environment.
|
|
@@ -103,6 +105,17 @@ Inspect `git status` before publishing. Stage and commit only files changed for
|
|
|
103
105
|
the current task, preserve unrelated worktree changes, then push the task's
|
|
104
106
|
current branch.
|
|
105
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
|
+
|
|
106
119
|
## Commands
|
|
107
120
|
|
|
108
121
|
<!-- commands-table:start — generated by `bun run generate`, do not edit -->
|
|
@@ -152,6 +165,7 @@ current branch.
|
|
|
152
165
|
| `qawolf runner inspect variable` | read | Print a top-level variable's value from the running workflow |
|
|
153
166
|
| `qawolf runner keepalive` | read | Reset a runner's inactivity clock, for a caller that pauses between actions |
|
|
154
167
|
| `qawolf runner launch` | write | Launch an interactive runner and make it this directory's default |
|
|
168
|
+
| `qawolf runner list` | read | List the runners this directory holds that are still running |
|
|
155
169
|
| `qawolf runner promote-snapshot` | write | Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it |
|
|
156
170
|
| `qawolf runner run` | write | Run a flow on an interactive runner, shipping the flow and what it imports |
|
|
157
171
|
| `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file |
|
|
@@ -41,6 +41,32 @@ 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
|
+
## Knowing what you are holding
|
|
45
|
+
|
|
46
|
+
`qawolf runner list` names the runners this directory has launched that are
|
|
47
|
+
still running, and marks the one a command with no `--runner` would reach:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
id family default
|
|
51
|
+
tester-abc-main playwright yes
|
|
52
|
+
tester-abc-checkout playwright
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Every runner is looked up before it is listed, so a runner that idled out is
|
|
56
|
+
absent rather than reported. The lookup neither starts a runner nor resets an
|
|
57
|
+
inactivity clock, which is what separates `list` from `keepalive`: listing tells
|
|
58
|
+
you what is there and changes nothing, and holding a runner open is still
|
|
59
|
+
`keepalive`'s job. A lookup that cannot be answered fails the command rather
|
|
60
|
+
than returning a shorter list, because a short list reads as the whole truth.
|
|
61
|
+
|
|
62
|
+
Nothing is billed by listing, but everything in the list is billing. Terminate
|
|
63
|
+
what you are done with.
|
|
64
|
+
|
|
65
|
+
The list includes the runner named by `QAWOLF_RUNNER_ID` even though this
|
|
66
|
+
directory did not launch it, so a harness handed a runner sees it alongside the
|
|
67
|
+
ones it started itself. Use the `id` column with `--runner` to address any of
|
|
68
|
+
them; addressing one does not make it the default.
|
|
69
|
+
|
|
44
70
|
## The order that matters
|
|
45
71
|
|
|
46
72
|
A freshly launched runner has no screen. The virtual desktop starts with the
|
|
@@ -312,6 +338,18 @@ qawolf runner run flows/checkout.flow.ts --env-id staging
|
|
|
312
338
|
qawolf runner run flows/checkout.flow.ts --env-file .env
|
|
313
339
|
```
|
|
314
340
|
|
|
341
|
+
**A run with neither flag falls back to `QAWOLF_ENVIRONMENT`**, the same
|
|
342
|
+
variable `qawolf flows` reads, so one export covers both. The run says on
|
|
343
|
+
stderr which environment it picked up, because those variables reach your flow's
|
|
344
|
+
code and a run should never be given an environment silently. `--env-id` wins
|
|
345
|
+
over it, and `--env-file` suppresses it, so a run reading a dotenv file is not
|
|
346
|
+
handed a second environment on top.
|
|
347
|
+
|
|
348
|
+
```sh
|
|
349
|
+
export QAWOLF_ENVIRONMENT=staging
|
|
350
|
+
qawolf runner run flows/checkout.flow.ts # runs against staging
|
|
351
|
+
```
|
|
352
|
+
|
|
315
353
|
**Prefer `--env-id`.** QA Wolf reads and decrypts the environment itself, so the
|
|
316
354
|
values never leave the server, nothing has to be pulled to disk first, and no
|
|
317
355
|
size limit applies to them. It is the only way to run a flow whose environment
|