@qawolf/cli 1.26.0 → 1.27.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 CHANGED
@@ -8787,7 +8787,17 @@ var interactMessages = {
8787
8787
  actionMayHaveHappened: "The runner could not be reached, which does not mean the action was not performed: it may have stopped answering mid-action. Take a screenshot before repeating it.",
8788
8788
  actionNotJson: `Stdin did not hold a JSON action. Pipe one object, for example '{"type":"click","button":"left","x":480,"y":260}'.`,
8789
8789
  actionNotSupportedOnMobile: (type) => `A mobile runner has a touchscreen, so it cannot perform ${type} as asked. It taps with a left-button click, swipes with drag, and types into whatever the last tap focused.`,
8790
+ actionFailedScreenshotToStdout: "Its screen was written to stdout as a JPEG, so look at that rather than sending the action again.",
8791
+ actionFailedScreenshotUnwritten: (detail) => `Its screen could not be written, because ${detail}, so take one with qawolf runner screenshot rather than sending the action again.`,
8792
+ actionFailedScreenshotWritten: (path) => `Its screen was written to ${path}, so look at that rather than sending the action again.`,
8793
+ actionFailedWithoutScreenshot: "The runner answered without a screen, so nothing was written; take one with qawolf runner screenshot rather than sending the action again.",
8790
8794
  actionPerformed: (type) => `Performed ${type}.`,
8795
+ actionPerformedScreenshotNotAnImage: (type) => `Performed ${type}, but the screen that came with the answer was not a JPEG, so nothing was written. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead, and report it if it keeps happening.`,
8796
+ actionPerformedScreenshotToStdout: (type) => `Performed ${type} and wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.`,
8797
+ actionPerformedScreenshotStdoutUnwritable: (type, detail) => `Performed ${type}, but its screen could not be written to stdout: ${detail}. The action took effect, so do not repeat it: keep the pipe reading stdout open, or take the screen with qawolf runner screenshot --out <file>.`,
8798
+ actionPerformedScreenshotUnwritable: (type, path, detail) => `Performed ${type}, but its screen could not be written to "${path}": ${detail}. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot, giving --out a path this process can write to.`,
8799
+ actionPerformedScreenshotWritten: (type, path) => `Performed ${type} and wrote the runner's screen to ${path}.`,
8800
+ actionPerformedWithoutScreenshot: (type) => `Performed ${type}, but the runner answered without the screen it was asked for. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead. If this keeps happening, the platform or this CLI is behind the other: upgrade with npm install -g @qawolf/cli.`,
8791
8801
  actionAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
8792
8802
  screenshotAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
8793
8803
  runnerHasNoScreen: "This runner does not run a browser on a virtual desktop, so there is nothing about it to see or drive. Retrying will never help: launch a playwright runner instead.",
@@ -8799,8 +8809,11 @@ var interactMessages = {
8799
8809
  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.",
8800
8810
  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.",
8801
8811
  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.",
8812
+ stdoutIsATerminal: (flag) => `Stdout is a terminal, so the JPEG bytes would have nowhere to go. Redirect stdout to a file or pipe it into a reader, or give ${flag} a file path instead of "-".`,
8813
+ screenshotStdoutUnwritable: (detail) => `The screen was captured but could not be written to stdout: ${detail}. Keep the pipe reading stdout open, or give --out a file path instead of "-".`,
8802
8814
  screenshotUnwritable: (path, detail) => `The screen was captured but could not be written to "${path}": ${detail}. Give --out a path this process can write to.`,
8803
8815
  screenshotWritten: (path) => `Wrote the runner's screen to ${path}.`,
8816
+ screenshotWrittenToStdout: "Wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.",
8804
8817
  snippetEmpty: (path) => `"${path}" holds no code to evaluate.`,
8805
8818
  snippetErrored: (errorMessage) => errorMessage === undefined ? "The snippet threw and reported no message." : `The snippet threw: ${errorMessage}`,
8806
8819
  snippetFileUnreadable: (path) => `Could not read "${path}". Name a readable file, or "-" to read the snippet from stdin.`,
@@ -20551,7 +20564,7 @@ function startUpdateCheck(deps) {
20551
20564
  // package.json
20552
20565
  var package_default = {
20553
20566
  name: "@qawolf/cli",
20554
- version: "1.26.0",
20567
+ version: "1.27.0",
20555
20568
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
20556
20569
  keywords: [
20557
20570
  "automation",
@@ -32308,6 +32321,28 @@ function registerPublicApiCommands(program, signals, options = {}) {
32308
32321
  }
32309
32322
  }
32310
32323
 
32324
+ // src/domains/interactiveRunner/evaluateSnippetFailure.ts
32325
+ function describeEvaluateSnippetFailure(failure) {
32326
+ const { failureReason } = failure;
32327
+ switch (failureReason) {
32328
+ case "runner-cannot-evaluate-snippets":
32329
+ return {
32330
+ error: interactiveRunnerMessages.runnerCannotEvaluateSnippets,
32331
+ exitCode: exitCodes.invalidArgs
32332
+ };
32333
+ case "runner-unreachable":
32334
+ return {
32335
+ error: interactiveRunnerMessages.snippetRunnerUnreachable,
32336
+ exitCode: exitCodes.network
32337
+ };
32338
+ default:
32339
+ return {
32340
+ error: interactiveRunnerMessages.snippetAnsweredUnknown(failureReason),
32341
+ exitCode: exitCodes.network
32342
+ };
32343
+ }
32344
+ }
32345
+
32311
32346
  // src/domains/interactiveRunner/runnerCallOptions.ts
32312
32347
  var runnerCallOptions = { timeoutMs: 90000 };
32313
32348
 
@@ -32420,67 +32455,138 @@ function announceRunner(ctx, resolved) {
32420
32455
  }
32421
32456
  }
32422
32457
 
32423
- // src/domains/interactiveRunner/highlightSelector.ts
32424
- async function handleRunnerHighlightSelector(ctx, options, deps) {
32425
- const resolved = await resolveRunner(ctx, {
32426
- autoLaunch: false,
32427
- noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForHighlight,
32428
- runner: options.runner
32429
- }, deps);
32458
+ // src/core/interactiveRunner/runFiles.ts
32459
+ import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve11, sep as sep5 } from "node:path";
32460
+ function toCollectedPath(cwd, path) {
32461
+ const absolute = isAbsolute5(path) ? path : resolve11(cwd, path);
32462
+ return relative10(cwd, absolute).split(sep5).join("/");
32463
+ }
32464
+ var namedLargestFileCount = 3;
32465
+ function largestFiles(files) {
32466
+ return Object.entries(files).map(([path, content]) => ({
32467
+ byteLength: runFilesByteLength({ [path]: content }),
32468
+ path
32469
+ })).sort((a, b) => b.byteLength - a.byteLength).slice(0, namedLargestFileCount);
32470
+ }
32471
+ function checkPresenceAndSize(files, requiredPath) {
32472
+ if (!Object.hasOwn(files, requiredPath)) {
32473
+ return { path: requiredPath, type: "missing-file" };
32474
+ }
32475
+ const byteLength = runFilesByteLength(files);
32476
+ if (byteLength > maxRunFilesByteLength) {
32477
+ return {
32478
+ byteLength,
32479
+ largest: largestFiles(files),
32480
+ maxByteLength: maxRunFilesByteLength,
32481
+ type: "too-large"
32482
+ };
32483
+ }
32484
+ const encodedByteLength = Buffer.byteLength(JSON.stringify({ files, path: requiredPath }), "utf8");
32485
+ if (encodedByteLength > maxRunnerRequestEncodedByteLength) {
32486
+ return {
32487
+ byteLength: encodedByteLength,
32488
+ maxByteLength: maxRunnerRequestEncodedByteLength,
32489
+ type: "request-too-large"
32490
+ };
32491
+ }
32492
+ return { type: "ok" };
32493
+ }
32494
+ function checkRunFiles(files, entryPointPath) {
32495
+ const present = checkPresenceAndSize(files, entryPointPath);
32496
+ if (present.type !== "ok")
32497
+ return present;
32498
+ if (!Object.hasOwn(files, runPackageJsonPath)) {
32499
+ return { type: "missing-package-json" };
32500
+ }
32501
+ return { type: "ok" };
32502
+ }
32503
+ function checkSnippetFiles(files, contextFilePath) {
32504
+ return checkPresenceAndSize(files, contextFilePath);
32505
+ }
32506
+ function describeRunFilesCheck(check) {
32507
+ switch (check.type) {
32508
+ case "missing-file":
32509
+ return interactiveRunnerMessages.fileNotCollected(check.path);
32510
+ case "missing-package-json":
32511
+ return interactiveRunnerMessages.missingPackageJson;
32512
+ case "too-large":
32513
+ return interactiveRunnerMessages.filesTooLarge(check.byteLength, check.maxByteLength, check.largest);
32514
+ case "request-too-large":
32515
+ return interactiveRunnerMessages.requestTooLarge(check.byteLength, check.maxByteLength);
32516
+ }
32517
+ }
32518
+
32519
+ // src/domains/interactiveRunner/snippetScope.ts
32520
+ async function resolveSnippetScope(contextFile, deps) {
32521
+ if (contextFile === undefined) {
32522
+ return { filePath: undefined, files: undefined, ok: true };
32523
+ }
32524
+ const filePath = toCollectedPath(deps.cwd, contextFile);
32525
+ const { files } = await deps.collectRunFiles([filePath]);
32526
+ const check = checkSnippetFiles(files, filePath);
32527
+ if (check.type !== "ok") {
32528
+ return { error: describeRunFilesCheck(check), ok: false };
32529
+ }
32530
+ return { filePath, files, ok: true };
32531
+ }
32532
+
32533
+ // src/domains/interactiveRunner/evaluateSnippet.ts
32534
+ var stdinArgument = "-";
32535
+ async function readCode(source, deps) {
32536
+ const code = source === stdinArgument ? await deps.readStdin() : await deps.readFile(source).catch(() => {
32537
+ return;
32538
+ });
32539
+ if (code === undefined) {
32540
+ return {
32541
+ error: interactiveRunnerMessages.snippetFileUnreadable(source),
32542
+ ok: false
32543
+ };
32544
+ }
32545
+ if (code.trim() === "") {
32546
+ return source === stdinArgument ? { error: interactiveRunnerMessages.stdinEmptySnippet, ok: false } : { error: interactiveRunnerMessages.snippetEmpty(source), ok: false };
32547
+ }
32548
+ return { code, ok: true };
32549
+ }
32550
+ async function handleRunnerExec(ctx, options, deps) {
32551
+ const code = await readCode(options.source, deps);
32552
+ if (!code.ok)
32553
+ return { error: code.error, exitCode: exitCodes.invalidArgs };
32554
+ const scope = await resolveSnippetScope(options.contextFile, deps);
32555
+ if (!scope.ok)
32556
+ return { error: scope.error, exitCode: exitCodes.invalidArgs };
32557
+ const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
32430
32558
  if (resolved.type === "failed") {
32431
32559
  return { ...failureFields(resolved), exitCode: resolved.exitCode };
32432
32560
  }
32433
- const selector = options.selector ?? "";
32434
- const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.highlightSelector, { id: resolved.runnerId, selector }, runnerCallOptions);
32561
+ announceRunner(ctx, resolved);
32562
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.evaluateSnippet, {
32563
+ code: code.code,
32564
+ id: resolved.runnerId,
32565
+ ...scope.filePath === undefined ? {} : { filePath: scope.filePath },
32566
+ ...scope.files === undefined ? {} : { files: scope.files }
32567
+ }, runnerCallOptions);
32435
32568
  if (!result.ok) {
32436
- return { ...failureFields(result), exitCode: exitCodes.network };
32569
+ return {
32570
+ ...failureFields(result),
32571
+ exitCode: result.exitCode ?? exitCodes.network
32572
+ };
32437
32573
  }
32438
- const { outcome } = result.value;
32439
- switch (outcome) {
32440
- case "cleared":
32441
- ctx.ui.output(result.value, interactiveRunnerMessages.highlightCleared);
32442
- return;
32443
- case "success": {
32444
- const answer = result.value;
32445
- if (answer.status === "invalid") {
32446
- return {
32447
- error: interactiveRunnerMessages.highlightSelectorInvalid(answer.selector),
32448
- exitCode: exitCodes.invalidArgs
32449
- };
32450
- }
32451
- ctx.ui.output(answer, answer.status === "empty" ? interactiveRunnerMessages.highlightMatchedNothing(answer.selector) : interactiveRunnerMessages.highlightMatched(answer.matchCount, answer.targetPage));
32452
- return;
32453
- }
32454
- case "failure":
32455
- return describeFailure(result.value.failureReason);
32456
- default:
32457
- return {
32458
- error: interactiveRunnerMessages.highlightAnsweredUnknown(outcome),
32459
- exitCode: exitCodes.network
32460
- };
32574
+ if (result.value.outcome === "failure") {
32575
+ return describeEvaluateSnippetFailure(result.value);
32461
32576
  }
32462
- }
32463
- function describeFailure(failureReason) {
32464
- switch (failureReason) {
32465
- case "no-answer":
32466
- return {
32467
- error: interactiveRunnerMessages.highlightNoAnswer,
32468
- exitCode: exitCodes.network
32469
- };
32470
- case "runner-cannot-highlight-selectors":
32471
- return {
32472
- error: interactiveRunnerMessages.runnerCannotHighlightSelectors,
32473
- exitCode: exitCodes.invalidArgs
32474
- };
32475
- case "runner-unreachable":
32577
+ switch (result.value.result) {
32578
+ case "success":
32579
+ ctx.ui.output({ outcome: "evaluated", result: "success" }, interactiveRunnerMessages.snippetRan);
32580
+ return;
32581
+ case "stopped":
32476
32582
  return {
32477
- error: interactiveRunnerMessages.runnerUnreachable,
32478
- exitCode: exitCodes.network
32583
+ error: interactiveRunnerMessages.snippetStopped,
32584
+ exitCode: exitCodes.testFailure
32479
32585
  };
32480
- default:
32586
+ case "error":
32481
32587
  return {
32482
- error: interactiveRunnerMessages.highlightAnsweredUnknown(failureReason),
32483
- exitCode: exitCodes.network
32588
+ error: interactiveRunnerMessages.snippetErrored(result.value.errorMessage),
32589
+ exitCode: exitCodes.testFailure
32484
32590
  };
32485
32591
  }
32486
32592
  }
@@ -32823,19 +32929,38 @@ function makeRunnerStore(options) {
32823
32929
  // src/shell/interactiveRunner/writeScreenshot.ts
32824
32930
  import { dirname as dirname16 } from "node:path";
32825
32931
  var jpegStartOfImage = [255, 216, 255];
32932
+ var stdoutPath = "-";
32826
32933
  async function writeScreenshot(options) {
32827
32934
  const bytes = Buffer.from(options.imageJpegBase64, "base64");
32828
32935
  if (jpegStartOfImage.some((byte, index) => bytes[index] !== byte)) {
32829
32936
  return { ok: false, reason: "not-a-jpeg" };
32830
32937
  }
32831
32938
  try {
32832
- await options.fs.mkdir(dirname16(options.path), { recursive: true });
32833
- await options.fs.writeFile(options.path, bytes);
32939
+ if (options.path === stdoutPath) {
32940
+ await writeToStdout(options.stdout ?? process.stdout, bytes);
32941
+ } else {
32942
+ await options.fs.mkdir(dirname16(options.path), { recursive: true });
32943
+ await options.fs.writeFile(options.path, bytes);
32944
+ }
32834
32945
  } catch (error) {
32835
32946
  return { detail: errorMessage(error), ok: false, reason: "unwritable" };
32836
32947
  }
32837
32948
  return { ok: true };
32838
32949
  }
32950
+ function writeToStdout(stdout, bytes) {
32951
+ return new Promise((resolve, reject) => {
32952
+ const onError = (error) => reject(error);
32953
+ stdout.once("error", onError);
32954
+ stdout.write(bytes, (error) => {
32955
+ if (error) {
32956
+ reject(error);
32957
+ return;
32958
+ }
32959
+ stdout.off("error", onError);
32960
+ resolve();
32961
+ });
32962
+ });
32963
+ }
32839
32964
 
32840
32965
  // src/shell/stdin.ts
32841
32966
  async function readStdin2() {
@@ -32876,6 +33001,81 @@ function runnerDeps(ctx) {
32876
33001
  });
32877
33002
  }
32878
33003
 
33004
+ // src/commands/runner/exec.register.ts
33005
+ var execExamples = `
33006
+ Examples:
33007
+ $ qawolf runner exec snippet.ts
33008
+ $ echo 'console.log(await page.title())' | qawolf runner exec -
33009
+ $ qawolf runner exec snippet.ts --file flows/checkout.flow.ts`;
33010
+ function registerRunnerExecCommand(runner, signals) {
33011
+ declareCommandKind(runner.command("exec <file>"), "write").description("Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin").option("--file <path>", "File whose scope the snippet is evaluated in; it and the directory's other files travel with it").option("--runner <id>", runnerFlagDescription).addHelpText("after", execExamples).action((file, opts, command) => withAuthContext(signals, (ctx) => handleRunnerExec(ctx, { contextFile: opts.file, runner: opts.runner, source: file }, runnerDeps(ctx)))(opts, command));
33012
+ }
33013
+
33014
+ // src/domains/interactiveRunner/highlightSelector.ts
33015
+ async function handleRunnerHighlightSelector(ctx, options, deps) {
33016
+ const resolved = await resolveRunner(ctx, {
33017
+ autoLaunch: false,
33018
+ noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForHighlight,
33019
+ runner: options.runner
33020
+ }, deps);
33021
+ if (resolved.type === "failed") {
33022
+ return { ...failureFields(resolved), exitCode: resolved.exitCode };
33023
+ }
33024
+ const selector = options.selector ?? "";
33025
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.highlightSelector, { id: resolved.runnerId, selector }, runnerCallOptions);
33026
+ if (!result.ok) {
33027
+ return { ...failureFields(result), exitCode: exitCodes.network };
33028
+ }
33029
+ const { outcome } = result.value;
33030
+ switch (outcome) {
33031
+ case "cleared":
33032
+ ctx.ui.output(result.value, interactiveRunnerMessages.highlightCleared);
33033
+ return;
33034
+ case "success": {
33035
+ const answer = result.value;
33036
+ if (answer.status === "invalid") {
33037
+ return {
33038
+ error: interactiveRunnerMessages.highlightSelectorInvalid(answer.selector),
33039
+ exitCode: exitCodes.invalidArgs
33040
+ };
33041
+ }
33042
+ ctx.ui.output(answer, answer.status === "empty" ? interactiveRunnerMessages.highlightMatchedNothing(answer.selector) : interactiveRunnerMessages.highlightMatched(answer.matchCount, answer.targetPage));
33043
+ return;
33044
+ }
33045
+ case "failure":
33046
+ return describeFailure(result.value.failureReason);
33047
+ default:
33048
+ return {
33049
+ error: interactiveRunnerMessages.highlightAnsweredUnknown(outcome),
33050
+ exitCode: exitCodes.network
33051
+ };
33052
+ }
33053
+ }
33054
+ function describeFailure(failureReason) {
33055
+ switch (failureReason) {
33056
+ case "no-answer":
33057
+ return {
33058
+ error: interactiveRunnerMessages.highlightNoAnswer,
33059
+ exitCode: exitCodes.network
33060
+ };
33061
+ case "runner-cannot-highlight-selectors":
33062
+ return {
33063
+ error: interactiveRunnerMessages.runnerCannotHighlightSelectors,
33064
+ exitCode: exitCodes.invalidArgs
33065
+ };
33066
+ case "runner-unreachable":
33067
+ return {
33068
+ error: interactiveRunnerMessages.runnerUnreachable,
33069
+ exitCode: exitCodes.network
33070
+ };
33071
+ default:
33072
+ return {
33073
+ error: interactiveRunnerMessages.highlightAnsweredUnknown(failureReason),
33074
+ exitCode: exitCodes.network
33075
+ };
33076
+ }
33077
+ }
33078
+
32879
33079
  // src/commands/runner/highlightSelector.register.ts
32880
33080
  var highlightExamples = `
32881
33081
  Examples:
@@ -33265,162 +33465,10 @@ function registerRunnerInspectCommands(runner, signals) {
33265
33465
  registerRunnerInspectMobileCommands(inspect, signals);
33266
33466
  }
33267
33467
 
33268
- // src/domains/interactiveRunner/evaluateSnippetFailure.ts
33269
- function describeEvaluateSnippetFailure(failure) {
33270
- const { failureReason } = failure;
33271
- switch (failureReason) {
33272
- case "runner-cannot-evaluate-snippets":
33273
- return {
33274
- error: interactiveRunnerMessages.runnerCannotEvaluateSnippets,
33275
- exitCode: exitCodes.invalidArgs
33276
- };
33277
- case "runner-unreachable":
33278
- return {
33279
- error: interactiveRunnerMessages.snippetRunnerUnreachable,
33280
- exitCode: exitCodes.network
33281
- };
33282
- default:
33283
- return {
33284
- error: interactiveRunnerMessages.snippetAnsweredUnknown(failureReason),
33285
- exitCode: exitCodes.network
33286
- };
33287
- }
33288
- }
33289
-
33290
- // src/core/interactiveRunner/runFiles.ts
33291
- import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve11, sep as sep5 } from "node:path";
33292
- function toCollectedPath(cwd, path) {
33293
- const absolute = isAbsolute5(path) ? path : resolve11(cwd, path);
33294
- return relative10(cwd, absolute).split(sep5).join("/");
33295
- }
33296
- var namedLargestFileCount = 3;
33297
- function largestFiles(files) {
33298
- return Object.entries(files).map(([path, content]) => ({
33299
- byteLength: runFilesByteLength({ [path]: content }),
33300
- path
33301
- })).sort((a, b) => b.byteLength - a.byteLength).slice(0, namedLargestFileCount);
33302
- }
33303
- function checkPresenceAndSize(files, requiredPath) {
33304
- if (!Object.hasOwn(files, requiredPath)) {
33305
- return { path: requiredPath, type: "missing-file" };
33306
- }
33307
- const byteLength = runFilesByteLength(files);
33308
- if (byteLength > maxRunFilesByteLength) {
33309
- return {
33310
- byteLength,
33311
- largest: largestFiles(files),
33312
- maxByteLength: maxRunFilesByteLength,
33313
- type: "too-large"
33314
- };
33315
- }
33316
- const encodedByteLength = Buffer.byteLength(JSON.stringify({ files, path: requiredPath }), "utf8");
33317
- if (encodedByteLength > maxRunnerRequestEncodedByteLength) {
33318
- return {
33319
- byteLength: encodedByteLength,
33320
- maxByteLength: maxRunnerRequestEncodedByteLength,
33321
- type: "request-too-large"
33322
- };
33323
- }
33324
- return { type: "ok" };
33325
- }
33326
- function checkRunFiles(files, entryPointPath) {
33327
- const present = checkPresenceAndSize(files, entryPointPath);
33328
- if (present.type !== "ok")
33329
- return present;
33330
- if (!Object.hasOwn(files, runPackageJsonPath)) {
33331
- return { type: "missing-package-json" };
33332
- }
33333
- return { type: "ok" };
33334
- }
33335
- function checkSnippetFiles(files, contextFilePath) {
33336
- return checkPresenceAndSize(files, contextFilePath);
33337
- }
33338
- function describeRunFilesCheck(check) {
33339
- switch (check.type) {
33340
- case "missing-file":
33341
- return interactiveRunnerMessages.fileNotCollected(check.path);
33342
- case "missing-package-json":
33343
- return interactiveRunnerMessages.missingPackageJson;
33344
- case "too-large":
33345
- return interactiveRunnerMessages.filesTooLarge(check.byteLength, check.maxByteLength, check.largest);
33346
- case "request-too-large":
33347
- return interactiveRunnerMessages.requestTooLarge(check.byteLength, check.maxByteLength);
33348
- }
33349
- }
33350
-
33351
- // src/domains/interactiveRunner/snippetScope.ts
33352
- async function resolveSnippetScope(contextFile, deps) {
33353
- if (contextFile === undefined) {
33354
- return { filePath: undefined, files: undefined, ok: true };
33355
- }
33356
- const filePath = toCollectedPath(deps.cwd, contextFile);
33357
- const { files } = await deps.collectRunFiles([filePath]);
33358
- const check = checkSnippetFiles(files, filePath);
33359
- if (check.type !== "ok") {
33360
- return { error: describeRunFilesCheck(check), ok: false };
33361
- }
33362
- return { filePath, files, ok: true };
33363
- }
33364
-
33365
- // src/domains/interactiveRunner/evaluateSnippet.ts
33366
- var stdinArgument = "-";
33367
- async function readCode(source, deps) {
33368
- const code = source === stdinArgument ? await deps.readStdin() : await deps.readFile(source).catch(() => {
33369
- return;
33370
- });
33371
- if (code === undefined) {
33372
- return {
33373
- error: interactiveRunnerMessages.snippetFileUnreadable(source),
33374
- ok: false
33375
- };
33376
- }
33377
- if (code.trim() === "") {
33378
- return source === stdinArgument ? { error: interactiveRunnerMessages.stdinEmptySnippet, ok: false } : { error: interactiveRunnerMessages.snippetEmpty(source), ok: false };
33379
- }
33380
- return { code, ok: true };
33381
- }
33382
- async function handleRunnerExec(ctx, options, deps) {
33383
- const code = await readCode(options.source, deps);
33384
- if (!code.ok)
33385
- return { error: code.error, exitCode: exitCodes.invalidArgs };
33386
- const scope = await resolveSnippetScope(options.contextFile, deps);
33387
- if (!scope.ok)
33388
- return { error: scope.error, exitCode: exitCodes.invalidArgs };
33389
- const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
33390
- if (resolved.type === "failed") {
33391
- return { ...failureFields(resolved), exitCode: resolved.exitCode };
33392
- }
33393
- announceRunner(ctx, resolved);
33394
- const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.evaluateSnippet, {
33395
- code: code.code,
33396
- id: resolved.runnerId,
33397
- ...scope.filePath === undefined ? {} : { filePath: scope.filePath },
33398
- ...scope.files === undefined ? {} : { files: scope.files }
33399
- }, runnerCallOptions);
33400
- if (!result.ok) {
33401
- return {
33402
- ...failureFields(result),
33403
- exitCode: result.exitCode ?? exitCodes.network
33404
- };
33405
- }
33406
- if (result.value.outcome === "failure") {
33407
- return describeEvaluateSnippetFailure(result.value);
33408
- }
33409
- switch (result.value.result) {
33410
- case "success":
33411
- ctx.ui.output({ outcome: "evaluated", result: "success" }, interactiveRunnerMessages.snippetRan);
33412
- return;
33413
- case "stopped":
33414
- return {
33415
- error: interactiveRunnerMessages.snippetStopped,
33416
- exitCode: exitCodes.testFailure
33417
- };
33418
- case "error":
33419
- return {
33420
- error: interactiveRunnerMessages.snippetErrored(result.value.errorMessage),
33421
- exitCode: exitCodes.testFailure
33422
- };
33423
- }
33468
+ // src/core/sentences.ts
33469
+ function appendSentence(text, sentence) {
33470
+ const punctuated = /[.!?]$/.test(text.trimEnd()) ? text.trimEnd() : `${text.trimEnd()}.`;
33471
+ return `${punctuated} ${sentence}`;
33424
33472
  }
33425
33473
 
33426
33474
  // src/domains/interactiveRunner/performActionFailure.ts
@@ -33467,6 +33515,56 @@ function describePerformActionFailure(options) {
33467
33515
  }
33468
33516
  }
33469
33517
 
33518
+ // src/domains/interactiveRunner/performActionScreenshot.ts
33519
+ async function writeActionScreenshot(ctx, options, deps) {
33520
+ const { type } = options.action;
33521
+ if (options.imageJpegBase64 === undefined) {
33522
+ return {
33523
+ error: interactiveRunnerMessages.actionPerformedWithoutScreenshot(type),
33524
+ exitCode: exitCodes.network
33525
+ };
33526
+ }
33527
+ const written = await deps.writeScreenshot({
33528
+ imageJpegBase64: options.imageJpegBase64,
33529
+ path: options.out
33530
+ });
33531
+ if (!written.ok) {
33532
+ return written.reason === "not-a-jpeg" ? {
33533
+ error: interactiveRunnerMessages.actionPerformedScreenshotNotAnImage(type),
33534
+ exitCode: exitCodes.network
33535
+ } : {
33536
+ error: options.out === stdoutPath ? interactiveRunnerMessages.actionPerformedScreenshotStdoutUnwritable(type, written.detail) : interactiveRunnerMessages.actionPerformedScreenshotUnwritable(type, options.out, written.detail),
33537
+ exitCode: exitCodes.network
33538
+ };
33539
+ }
33540
+ if (options.out === stdoutPath) {
33541
+ ctx.ui.success(interactiveRunnerMessages.actionPerformedScreenshotToStdout(type));
33542
+ return;
33543
+ }
33544
+ ctx.ui.output({ action: options.action, outcome: "success", screenshotPath: options.out }, interactiveRunnerMessages.actionPerformedScreenshotWritten(type, options.out));
33545
+ return;
33546
+ }
33547
+ async function addFailureScreenshot(options, deps) {
33548
+ const note = await describeFailureScreenshot(options, deps);
33549
+ return {
33550
+ ...options.failure,
33551
+ error: appendSentence(options.failure.error, note)
33552
+ };
33553
+ }
33554
+ async function describeFailureScreenshot(options, deps) {
33555
+ if (options.imageJpegBase64 === undefined) {
33556
+ return interactiveRunnerMessages.actionFailedWithoutScreenshot;
33557
+ }
33558
+ const written = await deps.writeScreenshot({
33559
+ imageJpegBase64: options.imageJpegBase64,
33560
+ path: options.out
33561
+ });
33562
+ if (!written.ok) {
33563
+ return interactiveRunnerMessages.actionFailedScreenshotUnwritten(written.reason === "not-a-jpeg" ? "it did not arrive as a JPEG" : written.detail);
33564
+ }
33565
+ return options.out === stdoutPath ? interactiveRunnerMessages.actionFailedScreenshotToStdout : interactiveRunnerMessages.actionFailedScreenshotWritten(options.out);
33566
+ }
33567
+
33470
33568
  // src/core/interactiveRunner/browserAction.ts
33471
33569
  function parseJsonPath(path) {
33472
33570
  try {
@@ -33531,30 +33629,56 @@ async function handleRunnerAct(ctx, options, deps) {
33531
33629
  const built = await readAction(options.type, options.flags, deps);
33532
33630
  if (!built.ok)
33533
33631
  return { error: built.error, exitCode: exitCodes.invalidArgs };
33632
+ if (options.screenshot === stdoutPath && ctx.outputMode === "human") {
33633
+ return {
33634
+ error: interactiveRunnerMessages.stdoutIsATerminal("--screenshot"),
33635
+ exitCode: exitCodes.invalidArgs
33636
+ };
33637
+ }
33534
33638
  const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
33535
33639
  if (resolved.type === "failed") {
33536
33640
  return { ...failureFields(resolved), exitCode: resolved.exitCode };
33537
33641
  }
33538
33642
  announceRunner(ctx, resolved);
33539
- const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.performAction, { action: built.action, id: resolved.runnerId }, runnerCallOptions);
33643
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.performAction, {
33644
+ action: built.action,
33645
+ id: resolved.runnerId,
33646
+ ...options.screenshot === undefined ? {} : { withScreenshot: true }
33647
+ }, runnerCallOptions);
33540
33648
  if (!result.ok) {
33541
33649
  const fields = failureFields(result);
33542
33650
  return {
33543
33651
  ...fields,
33544
33652
  ...result.mayHaveArrived ? {
33545
- error: `${fields.error} ${interactiveRunnerMessages.actionMayHaveHappened}`
33653
+ error: appendSentence(fields.error, interactiveRunnerMessages.actionMayHaveHappened)
33546
33654
  } : {},
33547
33655
  exitCode: exitCodes.network
33548
33656
  };
33549
33657
  }
33550
- if (result.value.outcome === "success") {
33658
+ const answer = result.value;
33659
+ if (answer.outcome === "success") {
33660
+ if (options.screenshot !== undefined) {
33661
+ return writeActionScreenshot(ctx, {
33662
+ action: built.action,
33663
+ imageJpegBase64: answer.imageJpegBase64,
33664
+ out: options.screenshot
33665
+ }, deps);
33666
+ }
33551
33667
  ctx.ui.output({ action: built.action, outcome: "success" }, interactiveRunnerMessages.actionPerformed(built.action.type));
33552
33668
  return;
33553
33669
  }
33554
- return describePerformActionFailure({
33670
+ const failure = describePerformActionFailure({
33555
33671
  actionType: built.action.type,
33556
- failure: result.value
33672
+ failure: answer
33557
33673
  });
33674
+ if (options.screenshot === undefined || answer.failureReason !== "action-failed") {
33675
+ return failure;
33676
+ }
33677
+ return addFailureScreenshot({
33678
+ failure,
33679
+ imageJpegBase64: answer.imageJpegBase64,
33680
+ out: options.screenshot
33681
+ }, deps);
33558
33682
  }
33559
33683
 
33560
33684
  // src/domains/interactiveRunner/takeScreenshot.ts
@@ -33567,6 +33691,12 @@ async function handleRunnerScreenshot(ctx, options, deps) {
33567
33691
  if (resolved.type === "failed") {
33568
33692
  return { ...failureFields(resolved), exitCode: resolved.exitCode };
33569
33693
  }
33694
+ if (options.out === stdoutPath && ctx.outputMode === "human") {
33695
+ return {
33696
+ error: interactiveRunnerMessages.stdoutIsATerminal("--out"),
33697
+ exitCode: exitCodes.invalidArgs
33698
+ };
33699
+ }
33570
33700
  const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.takeScreenshot, { id: resolved.runnerId }, runnerCallOptions);
33571
33701
  if (!result.ok) {
33572
33702
  return { ...failureFields(result), exitCode: exitCodes.network };
@@ -33576,16 +33706,13 @@ async function handleRunnerScreenshot(ctx, options, deps) {
33576
33706
  imageJpegBase64: result.value.imageJpegBase64,
33577
33707
  path: options.out
33578
33708
  });
33579
- if (!written.ok) {
33580
- return written.reason === "not-a-jpeg" ? {
33581
- error: interactiveRunnerMessages.screenshotNotAnImage,
33582
- exitCode: exitCodes.network
33583
- } : {
33584
- error: interactiveRunnerMessages.screenshotUnwritable(options.out, written.detail),
33585
- exitCode: exitCodes.invalidArgs
33586
- };
33709
+ if (!written.ok)
33710
+ return describeUnwritten(written, options.out);
33711
+ if (options.out === stdoutPath) {
33712
+ ctx.ui.success(interactiveRunnerMessages.screenshotWrittenToStdout);
33713
+ } else {
33714
+ ctx.ui.output({ outcome: "success", path: options.out }, interactiveRunnerMessages.screenshotWritten(options.out));
33587
33715
  }
33588
- ctx.ui.output({ outcome: "success", path: options.out }, interactiveRunnerMessages.screenshotWritten(options.out));
33589
33716
  return;
33590
33717
  }
33591
33718
  const { failureReason } = result.value;
@@ -33618,13 +33745,27 @@ async function handleRunnerScreenshot(ctx, options, deps) {
33618
33745
  }
33619
33746
  }
33620
33747
  }
33748
+ function describeUnwritten(written, out) {
33749
+ if (written.reason === "not-a-jpeg") {
33750
+ return {
33751
+ error: interactiveRunnerMessages.screenshotNotAnImage,
33752
+ exitCode: exitCodes.network
33753
+ };
33754
+ }
33755
+ return {
33756
+ error: out === stdoutPath ? interactiveRunnerMessages.screenshotStdoutUnwritable(written.detail) : interactiveRunnerMessages.screenshotUnwritable(out, written.detail),
33757
+ exitCode: exitCodes.invalidArgs
33758
+ };
33759
+ }
33621
33760
 
33622
33761
  // src/commands/runner/interact.register.ts
33623
33762
  var defaultScreenshotPath = "screenshot.jpg";
33624
33763
  var screenshotExamples = `
33625
33764
  Examples:
33626
33765
  $ qawolf runner screenshot
33627
- $ qawolf runner screenshot --out screens/step-3.jpg`;
33766
+ $ qawolf runner screenshot --out screens/step-3.jpg
33767
+ $ qawolf runner screenshot --out - > step-3.jpg
33768
+ $ qawolf runner screenshot --out - | my-vision-tool`;
33628
33769
  var actExamples = `
33629
33770
  Examples:
33630
33771
  $ qawolf runner act click --button left --x 480 --y 260
@@ -33632,15 +33773,12 @@ Examples:
33632
33773
  $ qawolf runner act keypress --keys Control a
33633
33774
  $ qawolf runner act navigate --url https://example.com
33634
33775
  $ qawolf runner act drag --path '[{"x":10,"y":20},{"x":80,"y":90}]'
33635
- $ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act -`;
33636
- var execExamples = `
33637
- Examples:
33638
- $ qawolf runner exec snippet.ts
33639
- $ echo 'console.log(await page.title())' | qawolf runner exec -
33640
- $ qawolf runner exec snippet.ts --file flows/checkout.flow.ts`;
33776
+ $ qawolf runner act click --button left --x 480 --y 260 --screenshot step-4.jpg
33777
+ $ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act -
33778
+ $ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act - --screenshot - > step-5.jpg`;
33641
33779
  function registerRunnerInteractCommands(runner, signals) {
33642
- 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));
33643
- 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((action, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
33780
+ declareCommandKind(runner.command("screenshot"), "read").description("Save a JPEG of an interactive runner's screen to a file, or write it to stdout with --out -").option("--out <path>", "File to write the image to. - writes the JPEG bytes to stdout on their own and moves the confirmation, JSON included, to stderr", 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));
33781
+ 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("--screenshot <path>", "Also save a JPEG of the screen, taken after the action, to this file, in place of a separate screenshot. An action that did not take effect answers with one too. - writes it to stdout and moves the confirmation, JSON included, to stderr").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((action, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
33644
33782
  flags: {
33645
33783
  button: opts.button,
33646
33784
  keys: opts.keys,
@@ -33653,9 +33791,9 @@ function registerRunnerInteractCommands(runner, signals) {
33653
33791
  y: opts.y
33654
33792
  },
33655
33793
  runner: opts.runner,
33794
+ screenshot: opts.screenshot,
33656
33795
  type: action
33657
33796
  }, runnerDeps(ctx)))(opts, command));
33658
- declareCommandKind(runner.command("exec <file>"), "write").description("Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin").option("--file <path>", "File whose scope the snippet is evaluated in; it and the directory's other files travel with it").option("--runner <id>", runnerFlagDescription).addHelpText("after", execExamples).action((file, opts, command) => withAuthContext(signals, (ctx) => handleRunnerExec(ctx, { contextFile: opts.file, runner: opts.runner, source: file }, runnerDeps(ctx)))(opts, command));
33659
33797
  }
33660
33798
 
33661
33799
  // src/domains/interactiveRunner/readJournal.ts
@@ -34610,6 +34748,7 @@ function registerRunnerCommand(program, signals) {
34610
34748
  registerRunCommand(runner, signals);
34611
34749
  registerRunnerEventsCommand(runner, signals);
34612
34750
  registerRunnerInteractCommands(runner, signals);
34751
+ registerRunnerExecCommand(runner, signals);
34613
34752
  registerRunnerInspectCommands(runner, signals);
34614
34753
  registerRunnerImportPackageCommand(runner, signals);
34615
34754
  registerRunnerHighlightSelectorCommand(runner, signals);
@@ -34643,4 +34782,4 @@ createProgram({ signals }).parseAsync().catch(() => {
34643
34782
  process.exitCode = 1;
34644
34783
  }).finally(() => exitWhenIdle(typeof process.exitCode === "number" ? process.exitCode : 0));
34645
34784
 
34646
- //# debugId=D197CB13AEBF087464756E2164756E21
34785
+ //# debugId=90417CF807BBB8C264756E2164756E21
@@ -9110,7 +9110,17 @@ var interactMessages = {
9110
9110
  actionMayHaveHappened: "The runner could not be reached, which does not mean the action was not performed: it may have stopped answering mid-action. Take a screenshot before repeating it.",
9111
9111
  actionNotJson: `Stdin did not hold a JSON action. Pipe one object, for example '{"type":"click","button":"left","x":480,"y":260}'.`,
9112
9112
  actionNotSupportedOnMobile: (type) => `A mobile runner has a touchscreen, so it cannot perform ${type} as asked. It taps with a left-button click, swipes with drag, and types into whatever the last tap focused.`,
9113
+ actionFailedScreenshotToStdout: "Its screen was written to stdout as a JPEG, so look at that rather than sending the action again.",
9114
+ actionFailedScreenshotUnwritten: (detail) => `Its screen could not be written, because ${detail}, so take one with qawolf runner screenshot rather than sending the action again.`,
9115
+ actionFailedScreenshotWritten: (path) => `Its screen was written to ${path}, so look at that rather than sending the action again.`,
9116
+ actionFailedWithoutScreenshot: "The runner answered without a screen, so nothing was written; take one with qawolf runner screenshot rather than sending the action again.",
9113
9117
  actionPerformed: (type) => `Performed ${type}.`,
9118
+ actionPerformedScreenshotNotAnImage: (type) => `Performed ${type}, but the screen that came with the answer was not a JPEG, so nothing was written. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead, and report it if it keeps happening.`,
9119
+ actionPerformedScreenshotToStdout: (type) => `Performed ${type} and wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.`,
9120
+ actionPerformedScreenshotStdoutUnwritable: (type, detail) => `Performed ${type}, but its screen could not be written to stdout: ${detail}. The action took effect, so do not repeat it: keep the pipe reading stdout open, or take the screen with qawolf runner screenshot --out <file>.`,
9121
+ actionPerformedScreenshotUnwritable: (type, path, detail) => `Performed ${type}, but its screen could not be written to "${path}": ${detail}. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot, giving --out a path this process can write to.`,
9122
+ actionPerformedScreenshotWritten: (type, path) => `Performed ${type} and wrote the runner's screen to ${path}.`,
9123
+ actionPerformedWithoutScreenshot: (type) => `Performed ${type}, but the runner answered without the screen it was asked for. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead. If this keeps happening, the platform or this CLI is behind the other: upgrade with npm install -g @qawolf/cli.`,
9114
9124
  actionAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
9115
9125
  screenshotAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
9116
9126
  runnerHasNoScreen: "This runner does not run a browser on a virtual desktop, so there is nothing about it to see or drive. Retrying will never help: launch a playwright runner instead.",
@@ -9122,8 +9132,11 @@ var interactMessages = {
9122
9132
  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.",
9123
9133
  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.",
9124
9134
  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.",
9135
+ stdoutIsATerminal: (flag) => `Stdout is a terminal, so the JPEG bytes would have nowhere to go. Redirect stdout to a file or pipe it into a reader, or give ${flag} a file path instead of "-".`,
9136
+ screenshotStdoutUnwritable: (detail) => `The screen was captured but could not be written to stdout: ${detail}. Keep the pipe reading stdout open, or give --out a file path instead of "-".`,
9125
9137
  screenshotUnwritable: (path, detail) => `The screen was captured but could not be written to "${path}": ${detail}. Give --out a path this process can write to.`,
9126
9138
  screenshotWritten: (path) => `Wrote the runner's screen to ${path}.`,
9139
+ screenshotWrittenToStdout: "Wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.",
9127
9140
  snippetEmpty: (path) => `"${path}" holds no code to evaluate.`,
9128
9141
  snippetErrored: (errorMessage) => errorMessage === undefined ? "The snippet threw and reported no message." : `The snippet threw: ${errorMessage}`,
9129
9142
  snippetFileUnreadable: (path) => `Could not read "${path}". Name a readable file, or "-" to read the snippet from stdin.`,
@@ -10553,19 +10566,38 @@ function isTimeoutError(err) {
10553
10566
 
10554
10567
  // src/shell/interactiveRunner/writeScreenshot.ts
10555
10568
  var jpegStartOfImage = [255, 216, 255];
10569
+ var stdoutPath = "-";
10556
10570
  async function writeScreenshot(options) {
10557
10571
  const bytes = Buffer.from(options.imageJpegBase64, "base64");
10558
10572
  if (jpegStartOfImage.some((byte, index) => bytes[index] !== byte)) {
10559
10573
  return { ok: false, reason: "not-a-jpeg" };
10560
10574
  }
10561
10575
  try {
10562
- await options.fs.mkdir(dirname3(options.path), { recursive: true });
10563
- await options.fs.writeFile(options.path, bytes);
10576
+ if (options.path === stdoutPath) {
10577
+ await writeToStdout(options.stdout ?? process.stdout, bytes);
10578
+ } else {
10579
+ await options.fs.mkdir(dirname3(options.path), { recursive: true });
10580
+ await options.fs.writeFile(options.path, bytes);
10581
+ }
10564
10582
  } catch (error) {
10565
10583
  return { detail: errorMessage(error), ok: false, reason: "unwritable" };
10566
10584
  }
10567
10585
  return { ok: true };
10568
10586
  }
10587
+ function writeToStdout(stdout, bytes) {
10588
+ return new Promise((resolve, reject) => {
10589
+ const onError = (error) => reject(error);
10590
+ stdout.once("error", onError);
10591
+ stdout.write(bytes, (error) => {
10592
+ if (error) {
10593
+ reject(error);
10594
+ return;
10595
+ }
10596
+ stdout.off("error", onError);
10597
+ resolve();
10598
+ });
10599
+ });
10600
+ }
10569
10601
 
10570
10602
  // src/shell/stdin.ts
10571
10603
  async function readStdin() {
@@ -12575,9 +12607,14 @@ function createPageVerbs({ platformClient }) {
12575
12607
  return {
12576
12608
  async act({
12577
12609
  action,
12578
- runnerId
12610
+ runnerId,
12611
+ withScreenshot
12579
12612
  }) {
12580
- return toSdkResult(await platformClient.callPublicApi(performAction, { action, id: runnerId }, runnerCallOptions));
12613
+ return toSdkResult(await platformClient.callPublicApi(performAction, {
12614
+ action,
12615
+ id: runnerId,
12616
+ ...withScreenshot === undefined ? {} : { withScreenshot }
12617
+ }, runnerCallOptions));
12581
12618
  },
12582
12619
  async highlightSelector({
12583
12620
  highlight,
@@ -13177,4 +13214,4 @@ export {
13177
13214
  createRunnerSdk
13178
13215
  };
13179
13216
 
13180
- //# debugId=1D5281D81CC3C81164756E2164756E21
13217
+ //# debugId=37E313924A61E7F564756E2164756E21
@@ -10,7 +10,7 @@ export declare function createRunnerSdk(options: RunnerSdkOptions): {
10
10
  list(): Promise<SdkResult<ListedRunner[]>>;
11
11
  evaluateSnippet({ runnerId, scope, source, }: import("./types.js").EvaluateSnippetRequest): Promise<SdkResult<import("./types.js").EvaluatedSnippet>>;
12
12
  importPackage({ name, runnerId, version, }: import("./types.js").ImportPackageRequest): Promise<SdkResult<import("./types.js").ImportedPackage>>;
13
- act({ action, runnerId, }: import("./types.js").ActRequest): Promise<SdkResult<import("./types.js").PerformedAction>>;
13
+ act({ action, runnerId, withScreenshot, }: import("./types.js").ActRequest): Promise<SdkResult<import("./types.js").PerformedAction>>;
14
14
  highlightSelector({ highlight, runnerId, }: import("./types.js").HighlightSelectorRequest): Promise<SdkResult<import("./types.js").HighlightedSelector>>;
15
15
  inspect({ request, runnerId, }: import("./types.js").InspectRequest): Promise<SdkResult<import("./types.js").Inspected>>;
16
16
  promoteSnapshot({ baselinePath, runnerId, screenshotPath, }: import("./types.js").PromoteSnapshotRequest): Promise<SdkResult<import("./types.js").PromotedSnapshot>>;
@@ -66,6 +66,13 @@ export type EventsRequest = RunnerRequest & {
66
66
  };
67
67
  export type ActRequest = RunnerRequest & {
68
68
  action: BrowserAction;
69
+ /**
70
+ * Ask the runner to answer with a screenshot taken after the action, on
71
+ * `imageJpegBase64`. One call instead of an act and a screenshot, with no
72
+ * fixed wait between them. An action that did not take effect answers with
73
+ * one too, and the screen can be absent even when asked for.
74
+ */
75
+ withScreenshot?: boolean;
69
76
  };
70
77
  export type EvaluateSnippetRequest = RunnerRequest & {
71
78
  scope: SnippetScope;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qawolf/cli",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
5
5
  "keywords": [
6
6
  "automation",
@@ -187,7 +187,7 @@ that `url`; never guess a route and never send a repository link in its place.
187
187
  | `qawolf runner list` | read | List the runners running on your team |
188
188
  | `qawolf runner promote-snapshot` | write | Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it |
189
189
  | `qawolf runner run` | write | Run a flow on an interactive runner, shipping the flow and what it imports |
190
- | `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file |
190
+ | `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file, or write it to stdout with --out - |
191
191
  | `qawolf runner stop-run` | write | Stop what a runner is currently executing, leaving the runner up |
192
192
  | `qawolf runner terminate` | write | End an interactive runner, and the pod it runs on with it |
193
193
  | `qawolf tag create` | write | Create a tag on the caller's team. Tags select flows in run.create. |
@@ -210,6 +210,12 @@ your actions into Playwright locators), `keepalive` to hold it open, and
210
210
  when done. Everything is a plain request to one host, so a shell with an API key
211
211
  and its own vision model can close the see-and-act loop with no other tooling.
212
212
 
213
+ When an action is followed by a look at the result, which in a see-and-act loop
214
+ is every action, pass `--screenshot <path>` to `act` (or `-` for stdout) instead
215
+ of calling `act` and then `screenshot`. One call performs the action and writes
216
+ the screen the runner answers with: half the calls per step, and no delay to
217
+ guess at between them.
218
+
213
219
  The full workflow is its own guide: how a runner is billed, why the first call
214
220
  must be a run, the order the commands go in, the see-and-act loop, `exec`, the
215
221
  recorder, reading history, staying alive, and an end-to-end example. **Read
@@ -127,7 +127,10 @@ vision loop on this surface.
127
127
 
128
128
  `qawolf runner screenshot --out page.jpg` writes a real JPEG to disk, decoded,
129
129
  because every coding harness can open an image file. Read it with whatever
130
- vision you have.
130
+ vision you have. `--out -` writes the JPEG bytes to stdout instead, on their own,
131
+ for a caller that is a process rather than an agent: the confirmation, and the
132
+ JSON line under `--json`, goes to stderr so nothing follows the image on stdout.
133
+ A terminal on stdout is refused: redirect or pipe it.
131
134
 
132
135
  `qawolf runner act <action>` performs exactly one action per call, in the
133
136
  computer-use tool vocabulary a vision model already emits: `click`,
@@ -139,6 +142,19 @@ can forward a tool call rather than translate it:
139
142
  echo '{"type":"click","button":"left","x":480,"y":260}' | qawolf runner act -
140
143
  ```
141
144
 
145
+ Every action in a see-and-act loop is followed by a look at the result, so ask
146
+ for it in the same call: `act ... --screenshot step-1.jpg` (or `--screenshot -`
147
+ for stdout) performs the action and writes the screen the runner answers with.
148
+ Prefer it over `act` and then `screenshot`: each step is one call instead of
149
+ two, with no delay to guess at between them. The screen the runner answers with
150
+ is taken once it has changed from just before the action or half a second has
151
+ passed, whichever comes first. An action that reached the screen and did not
152
+ take effect answers with one too, so a `1` from `act --screenshot` still leaves
153
+ you a picture of why the click missed. A `4` whose message starts with
154
+ "Performed" means the action happened and only the picture is missing: take a
155
+ `screenshot`, never send the action again to get it. As with
156
+ `screenshot --out -`, a terminal on stdout is refused.
157
+
142
158
  Coordinates are pixels on the same screenshot you just read. The runner serves
143
159
  one see-or-act request at a time, so decide what to do next from each answer
144
160
  rather than firing several. Bounds are checked before anything is sent, so an
@@ -565,10 +581,9 @@ export QAWOLF_RUNNER_ID=agent-1 # so no command below needs --runner
565
581
  qawolf runner launch --id agent-1 --json # --id, not the variable; read .alreadyRunning
566
582
  qawolf runner run flows/smoke.flow.ts --follow # starts the screen; exit 1 if it failed
567
583
 
568
- qawolf runner act navigate --url https://example.com/login
569
- qawolf runner screenshot --out page.jpg # then read page.jpg yourself
570
- qawolf runner act click --button left --x 480 --y 260
571
- qawolf runner act type --text "someone@example.com"
584
+ qawolf runner act navigate --url https://example.com/login --screenshot step-1.jpg # then read step-1.jpg yourself
585
+ qawolf runner act click --button left --x 480 --y 260 --screenshot step-2.jpg
586
+ qawolf runner act type --text "someone@example.com" --screenshot step-3.jpg
572
587
 
573
588
  qawolf runner inspect element-html --selector "#email"
574
589
  qawolf runner inspect variable --name cart | jq .total