@qawolf/cli 1.25.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.`,
@@ -9083,25 +9096,32 @@ function exit(code, message, proc = process) {
9083
9096
  }
9084
9097
  return proc.exit(code);
9085
9098
  }
9086
- function flushAndExit(code, proc = process, scheduleBackstop = (fn) => {
9087
- setTimeout(fn, 2000).unref();
9099
+ var backstopMs = 2000;
9100
+ function exitWhenIdle(code, proc = process, scheduleBackstop = (fn) => {
9101
+ setTimeout(fn, backstopMs).unref();
9088
9102
  }) {
9089
- let exited = false;
9090
- const exitOnce = () => {
9091
- if (exited)
9092
- return;
9093
- exited = true;
9094
- proc.exit(code);
9095
- };
9096
- let pending = 2;
9097
- const onFlushed = () => {
9098
- pending -= 1;
9099
- if (pending === 0)
9100
- exitOnce();
9103
+ proc.exitCode = code;
9104
+ scheduleBackstop(() => proc.exit(code));
9105
+ }
9106
+ var signalExitCodes = {
9107
+ SIGINT: 130,
9108
+ SIGTERM: 143
9109
+ };
9110
+ var signalGraceMs = 150;
9111
+ function scheduleSignalGrace(fn) {
9112
+ return setTimeout(fn, signalGraceMs);
9113
+ }
9114
+ function createSignalExit(deps) {
9115
+ const proc = deps.proc ?? process;
9116
+ const scheduleGrace = deps.scheduleGrace ?? scheduleSignalGrace;
9117
+ let signalled = false;
9118
+ return (signal) => () => {
9119
+ const code = signalExitCodes[signal];
9120
+ if (signalled)
9121
+ proc.exit(code);
9122
+ signalled = true;
9123
+ deps.shutdown(signal).catch(() => {}).finally(() => exitWhenIdle(code, proc, scheduleGrace));
9101
9124
  };
9102
- proc.stdout.write("", onFlushed);
9103
- proc.stderr.write("", onFlushed);
9104
- scheduleBackstop(exitOnce);
9105
9125
  }
9106
9126
 
9107
9127
  // src/shell/fs.ts
@@ -16384,6 +16404,10 @@ var makeEmailResourceSchema = () => resource({
16384
16404
  replyTo: array(namedAddressSchema).optional(),
16385
16405
  text: string2().optional().describe("The plain text body.")
16386
16406
  }, { urlFieldDescription: emailUrlFieldDescription });
16407
+ var makeEmailAddressResourceSchema = () => resource({
16408
+ address: string2().describe("The inbox address. Mail sent to any plus-suffixed form, such as inbox+abc@example.com, lands in the same inbox."),
16409
+ isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
16410
+ }, { urlFieldDescription: emailUrlFieldDescription });
16387
16411
 
16388
16412
  // node_modules/@qawolf/api-contracts/dist/v1/identity/organization.js
16389
16413
  var identityOrganization = object({
@@ -16607,6 +16631,8 @@ var runnerNameSchema = _enum([
16607
16631
  "android",
16608
16632
  "ios"
16609
16633
  ]);
16634
+ var runnerWorkspaceRequirement = "Required when the credential is not bound to a single workspace, such as a user API key or an OAuth connection whose organization owns several.";
16635
+ var runnerWorkspaceIdDescription = `The workspace the runner belongs to. ${runnerWorkspaceRequirement}`;
16610
16636
  var runnerIdSchema = string2().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "A runner id may contain only lowercase letters, digits and dashes, and must start and end with a letter or digit.");
16611
16637
  var makeRunnerSchema = () => object({
16612
16638
  gpuAccelerated: boolean2().describe("Whether the runner was placed on GPU hardware. Chosen by QA Wolf from the team's configuration and what the fleet can provide, not requested."),
@@ -16634,10 +16660,11 @@ var inspectRequestSchema = discriminatedUnion("what", [
16634
16660
  what: literal("variable")
16635
16661
  })
16636
16662
  ]);
16637
- var makeInspectOnRunnerContract = () => {
16663
+ var makeInspectOnRunnerContract = (ids) => {
16638
16664
  const input = object({
16639
16665
  id: runnerIdSchema.describe("Id of the runner to inspect."),
16640
- request: inspectRequestSchema
16666
+ request: inspectRequestSchema,
16667
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
16641
16668
  });
16642
16669
  const output = discriminatedUnion("outcome", [
16643
16670
  object({
@@ -16784,10 +16811,11 @@ var inspectMobileAnswerSchema = discriminatedUnion("what", [
16784
16811
  what: literal("elements")
16785
16812
  })
16786
16813
  ]);
16787
- var makeInspectMobileOnRunnerContract = () => {
16814
+ var makeInspectMobileOnRunnerContract = (ids) => {
16788
16815
  const input = object({
16789
16816
  id: runnerIdSchema.describe("Id of the runner to inspect."),
16790
- request: inspectMobileRequestSchema
16817
+ request: inspectMobileRequestSchema,
16818
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
16791
16819
  });
16792
16820
  const output = discriminatedUnion("outcome", [
16793
16821
  inspectMobileAnswerSchema,
@@ -16871,20 +16899,26 @@ var screenFailureReasons = [
16871
16899
  var screenNotReadyDescription = "`screen-not-ready` if the runner has a screen that cannot serve this instant. Retry in a second or two: the desktop restarts when a run changes the display size, and it serves one see-or-act request at a time, so a screenshot or action already in flight is the usual reason. Do not submit a run to clear this — a run may restart the display and discard what is on it.";
16872
16900
  var screenNeedsARunDescription = "`screen-needs-a-run` if the runner's virtual desktop has never started. Waiting will not change this and retrying is pointless — call `runner.runFlow` with a flow that opens a browser, then ask for the screen again. On a runner image with a browser that has never run anything, any `runner.performAction` also starts the browser itself. Evaluating a snippet does not start the desktop.";
16873
16901
  var runnerHasNoScreenDescription = "`runner-has-no-screen` if this runner is not one that runs a browser on a virtual desktop. Nothing about it can be seen or driven, and retrying will never help — launch a `playwright` runner instead.";
16902
+ var screenshotImageSchema = string2().min(1).describe("The screenshot, as a base64-encoded JPEG. Decode it and write the bytes to a `.jpg` file — writing this string to the file leaves you with base64 text rather than an image.");
16874
16903
 
16875
16904
  // node_modules/@qawolf/api-contracts/dist/v1/runner/performAction.js
16876
16905
  var unreachableDescription = "`runner-unreachable` if the runner could not be reached: it may still be starting, it may have terminated after inactivity, or it may have stopped answering mid-action. This does not mean the action was not performed — take a screenshot before repeating it.";
16877
16906
  var notSupportedOnMobileDescription = "`action-not-supported-on-mobile` if the runner is a mobile device and this action has no touchscreen equivalent: `double_click`, `scroll`, `move`, `keypress` and `navigate`, and a `click` whose `button` is not `left`, are all pointer-device concepts a touchscreen has nothing to offer for. `click` taps, `drag` swipes between its path's first and last point, and `type` types into whatever the last tap focused.";
16907
+ var withScreenshotDescription = "With `withScreenshot: true`, the answer also carries `imageJpegBase64`: a screenshot taken after the action, once the screen has changed from just before it or half a second has passed, whichever comes first. One call instead of `runner.performAction` followed by `runner.takeScreenshot`, with no fixed wait in between. Anything animating on the page counts as a change. The action itself is performed exactly as without the option; if the screen could serve no frame after it — a display restart, or a `navigate` on a desktop that is still starting — the action's result comes back without `imageJpegBase64`, so take a screenshot separately rather than repeating the action. On a mobile runner the screenshot is the device's own, taken right after a performed action.";
16878
16908
  var maxActionErrorMessageLength = 1000;
16879
- var makePerformActionOnRunnerContract = () => {
16909
+ var makePerformActionOnRunnerContract = (ids) => {
16880
16910
  const input = object({
16881
16911
  action: browserActionSchema.describe("The action to perform. Coordinates are pixels on the screenshot's own coordinate space."),
16882
- id: runnerIdSchema.describe("Id of the runner to act on.")
16912
+ id: runnerIdSchema.describe("Id of the runner to act on."),
16913
+ withScreenshot: boolean2().optional().describe("Also answer with a screenshot taken after the action, once the screen has changed or half a second has passed. Saves the separate `runner.takeScreenshot` call and its fixed wait."),
16914
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
16883
16915
  });
16916
+ const screenshotAfterAction = screenshotImageSchema.optional();
16884
16917
  const failure = discriminatedUnion("failureReason", [
16885
16918
  object({
16886
16919
  errorMessage: string2().max(maxActionErrorMessageLength).describe("What stopped the action from taking effect."),
16887
16920
  failureReason: literal("action-failed"),
16921
+ imageJpegBase64: screenshotAfterAction,
16888
16922
  outcome: literal("failure")
16889
16923
  }),
16890
16924
  makeRunnerFailureSchema([
@@ -16894,11 +16928,14 @@ var makePerformActionOnRunnerContract = () => {
16894
16928
  ])
16895
16929
  ]);
16896
16930
  const output = discriminatedUnion("outcome", [
16897
- object({ outcome: literal("success") }),
16931
+ object({
16932
+ imageJpegBase64: screenshotAfterAction,
16933
+ outcome: literal("success")
16934
+ }),
16898
16935
  failure
16899
16936
  ]);
16900
16937
  return {
16901
- description: `Perform one raw browser action on an interactive runner: click, double_click, move, drag, scroll, keypress, type, or navigate. Coordinates are whole pixels on the runner's virtual desktop, in the same space as \`runner.takeScreenshot\`. One action per request, and the runner serves one at a time. The action shapes follow the computer-use vocabulary, minus \`screenshot\` (use \`runner.takeScreenshot\`) and \`wait\` (delay on the caller's side). A success means the action took effect. \`action-failed\`, with a reason, if it reached the runner and did not take effect. On a runner image with a browser, the first action on a runner that has never run anything starts its browser and waits for it, so it can take up to a minute to answer — no \`runner.runFlow\` is needed before acting; if the browser is still starting when the wait runs out, the answer is \`screen-not-ready\` and retrying converges. On a mobile runner, the same \`screen-needs-a-run\` and \`screen-not-ready\` outcomes mean no Appium session has started yet, or it did not answer this instant; \`runner.runFlow\` is what starts one, same as a browser. \`screen-needs-a-run\` if the browser could not be started that way — usually a runner whose runs all finished without starting its desktop; call \`runner.runFlow\` with a flow that opens a browser. ${screenNotReadyDescription} A \`navigate\` does not go through the screen, so a screen that is not ready does not stop it. ${runnerHasNoScreenDescription} ${notSupportedOnMobileDescription} ${unreachableDescription}`,
16938
+ description: `Perform one raw browser action on an interactive runner: click, double_click, move, drag, scroll, keypress, type, or navigate. Coordinates are whole pixels on the runner's virtual desktop, in the same space as \`runner.takeScreenshot\`. One action per request, and the runner serves one at a time. The action shapes follow the computer-use vocabulary, minus \`screenshot\` (use \`runner.takeScreenshot\`) and \`wait\` (delay on the caller's side). A success means the action took effect. \`action-failed\`, with a reason, if it reached the runner and did not take effect. On a runner image with a browser, the first action on a runner that has never run anything starts its browser and waits for it, so it can take up to a minute to answer — no \`runner.runFlow\` is needed before acting; if the browser is still starting when the wait runs out, the answer is \`screen-not-ready\` and retrying converges. On a mobile runner, the same \`screen-needs-a-run\` and \`screen-not-ready\` outcomes mean no Appium session has started yet, or it did not answer this instant; \`runner.runFlow\` is what starts one, same as a browser. \`screen-needs-a-run\` if the browser could not be started that way — usually a runner whose runs all finished without starting its desktop; call \`runner.runFlow\` with a flow that opens a browser. ${screenNotReadyDescription} A \`navigate\` does not go through the screen, so a screen that is not ready does not stop it. ${runnerHasNoScreenDescription} ${notSupportedOnMobileDescription} ${withScreenshotDescription} ${unreachableDescription}`,
16902
16939
  input,
16903
16940
  kind: "write",
16904
16941
  name: "runner.performAction",
@@ -16924,9 +16961,11 @@ var makeAgentGetContract = (ids) => {
16924
16961
  replies: array(agentReplySchema).describe("Everything the AI has said so far, oldest first. A reply carrying choices is a question the work is blocked on."),
16925
16962
  sessionId: ids.chatSession,
16926
16963
  status: agentSessionStatusSchema
16927
- }, { urlFieldDescription: "Absolute URL of the session in the QA Wolf app." });
16964
+ }, {
16965
+ urlFieldDescription: "Absolute URL of the live session in the QA Wolf app."
16966
+ });
16928
16967
  return {
16929
- description: 'Read what the QA Wolf AI has said and whether it is still working. Poll this after agent.send until the status settles. A status of "waiting-for-you" means the last reply is a question the work is blocked on, and answering it with agent.send is what unblocks it. Replies accumulate, so a caller that polls repeatedly sees the earlier ones again.',
16968
+ description: 'Monitor a QA Wolf AI session by reading its status and replies. After agent.send, share the returned session URL before monitoring. Wait 30 to 60 seconds between checks; do not call this in a tight loop. Replies accumulate, so compare them with what you have already seen. Continue monitoring silently when the status and replies are unchanged; do not narrate waiting, announce the next check, or ask whether to keep monitoring. Report only substantive new progress, questions, blockers, or the final outcome. A status of "waiting-for-you" means the last reply is a question the work is blocked on, and answering it with agent.send is what unblocks it. Surface an explicit request for user input even if the status still says "working". Include the session URL when reporting a blocker or final outcome. On "completed", stop status checks and verify the requested result before claiming success. For new flows, validation, publication in the target environment, and readiness are separate checks; a Git push or final reply does not prove the flow is active. If every requested result is verified but status remains "working", report the mismatch and stop monitoring. Stop on "failed" or "cancelled" and report any confirmed partial result.',
16930
16969
  input,
16931
16970
  kind: "read",
16932
16971
  name: "agent.get",
@@ -16945,9 +16984,11 @@ var makeAgentSendContract = (ids) => {
16945
16984
  const output = resource({
16946
16985
  sessionId: ids.chatSession.describe("Follow the work by passing this to agent.get."),
16947
16986
  status: agentSessionStatusSchema
16948
- }, { urlFieldDescription: "Absolute URL of the session in the QA Wolf app." });
16987
+ }, {
16988
+ urlFieldDescription: "Absolute URL of the live session in the QA Wolf app."
16989
+ });
16949
16990
  return {
16950
- description: "Ask the QA Wolf AI to do a piece of work in plain language, such as covering a user journey, investigating a failing run, or fixing a broken flow. This is the one verb that starts work from nothing: every other write acts on a flow, run or issue that already exists. Answers as soon as the request is accepted, with the id to follow it by, because the work runs for minutes to tens of minutes. Poll agent.get for progress, and send here again to answer a question or add context to work already running.",
16991
+ description: "Start or continue work with the QA Wolf AI and return a live session URL to share with the user. Use it to cover a user journey, investigate a failing run, or fix a broken flow. This is the one verb that starts work from nothing: every other write acts on a flow, run or issue that already exists. Returns sessionId, status, and url as soon as the request is accepted; work can take minutes to tens of minutes. After each send, make the next action a normal user-visible assistant message containing the exact returned url, before any tool call or wait. Tool output and internal reasoning do not count as sharing the link. Do not run a timer or monitoring call alongside this send. Acceptance does not mean the work is complete. Then monitor the session with agent.get, reporting new progress, blockers, and the final outcome rather than unchanged status. Send here again to answer a question or add context to the same session.",
16951
16992
  input,
16952
16993
  kind: "write",
16953
16994
  name: "agent.send",
@@ -17050,10 +17091,7 @@ var makeListEmailAddressesContract = (ids) => {
17050
17091
  ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 50 })
17051
17092
  });
17052
17093
  const output = object({
17053
- addresses: array(resource({
17054
- address: string2().describe("The inbox address. Mail sent to any plus-suffixed form, such as inbox+abc@example.com, lands in the same inbox."),
17055
- isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
17056
- }, { urlFieldDescription: emailUrlFieldDescription })).describe("The workspace's permanent inbox addresses, alphabetical."),
17094
+ addresses: array(makeEmailAddressResourceSchema()).describe("The workspace's permanent inbox addresses, alphabetical."),
17057
17095
  nextCursor: nextCursorSchema
17058
17096
  });
17059
17097
  return {
@@ -17065,6 +17103,28 @@ var makeListEmailAddressesContract = (ids) => {
17065
17103
  };
17066
17104
  };
17067
17105
 
17106
+ // node_modules/@qawolf/api-contracts/dist/v1/email/registerAddress.js
17107
+ var hasNoPlusSuffix = (address) => {
17108
+ const [localPart] = address.split("@");
17109
+ return localPart !== undefined && !localPart.includes("+");
17110
+ };
17111
+ var makeRegisterEmailAddressContract = (ids) => {
17112
+ const input = object({
17113
+ address: email2().refine(hasNoPlusSuffix, {
17114
+ message: "Register the base address. Mail to a plus-suffixed form lands in the base inbox."
17115
+ }).describe("The inbox address to register. Its domain must be one QA Wolf serves for the workspace."),
17116
+ workspaceId: ids.workspace.optional().describe("The workspace to register the address in. Required when authenticating with an organization or user API key.")
17117
+ });
17118
+ const output = makeEmailAddressResourceSchema();
17119
+ return {
17120
+ description: "Register an inbox address for the workspace. Registering an address the workspace already has changes nothing. A refusal names the domains the workspace can use.",
17121
+ input,
17122
+ kind: "write",
17123
+ name: "email.registerAddress",
17124
+ output
17125
+ };
17126
+ };
17127
+
17068
17128
  // node_modules/@qawolf/api-contracts/dist/v1/email/send.js
17069
17129
  var recipientsSchema = array(email2()).max(50);
17070
17130
  var maxAttachments = 10;
@@ -17705,14 +17765,31 @@ var makeReattemptRunContract = (ids) => {
17705
17765
  };
17706
17766
  };
17707
17767
 
17768
+ // node_modules/@qawolf/api-contracts/dist/v1/run/stop.js
17769
+ var makeStopRunContract = (ids) => {
17770
+ const input = object({ runId: ids.run });
17771
+ const output = resource({
17772
+ runId: ids.run,
17773
+ status: _enum(["accepted", "already-finished", "execution-not-found"]).describe('"accepted" means stopping was requested; poll run.get for the final result. "already-finished" means the run has already finished and its result is unchanged. "execution-not-found" means the run exists but no execution was found to stop; it may still be starting or may no longer be available. No stop request was accepted in that case.')
17774
+ }, { urlFieldDescription: "Absolute URL of the run page." });
17775
+ return {
17776
+ description: "Stop a run, including its queued flows and automatic retries. Stopping is asynchronous. Repeated requests are safe, and finished runs keep their results. A run that is still being created returns not found; retry once run.get returns the run. If run.get returns a different runId, use that ID. Poll run.get for results.",
17777
+ input,
17778
+ kind: "write",
17779
+ name: "run.stop",
17780
+ output
17781
+ };
17782
+ };
17783
+
17708
17784
  // node_modules/@qawolf/api-contracts/dist/v1/runner/evaluateSnippet.js
17709
17785
  var maxSnippetCodeLength = 64 * 1024;
17710
- var makeEvaluateSnippetOnRunnerContract = () => {
17786
+ var makeEvaluateSnippetOnRunnerContract = (ids) => {
17711
17787
  const input = object({
17712
17788
  code: string2().min(1).max(maxSnippetCodeLength).describe("The code to evaluate against the runner's live page, in the scope of `filePath` when one is given."),
17713
17789
  filePath: runFilePathSchema.optional().describe("Path of the file the snippet is evaluated in the scope of. Omit to evaluate a snippet that imports nothing of yours."),
17714
17790
  files: runFilesSchema.optional().describe("The file named by `filePath` and everything it imports. A runner holds no copy of your project, so a snippet's scope has to travel with it."),
17715
- id: runnerIdSchema.describe("Id of the runner to evaluate the snippet on.")
17791
+ id: runnerIdSchema.describe("Id of the runner to evaluate the snippet on."),
17792
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17716
17793
  }).refine((request) => request.filePath === undefined || Object.hasOwn(request.files ?? {}, request.filePath), {
17717
17794
  error: "A snippet naming a file must carry that file in files.",
17718
17795
  path: ["files"]
@@ -17738,9 +17815,10 @@ var makeEvaluateSnippetOnRunnerContract = () => {
17738
17815
  };
17739
17816
 
17740
17817
  // node_modules/@qawolf/api-contracts/dist/v1/runner/get.js
17741
- var makeGetRunnerContract = () => {
17818
+ var makeGetRunnerContract = (ids) => {
17742
17819
  const input = object({
17743
- id: runnerIdSchema.describe("Id of the runner to look up.")
17820
+ id: runnerIdSchema.describe("Id of the runner to look up."),
17821
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17744
17822
  });
17745
17823
  const output = object({
17746
17824
  id: runnerIdSchema.describe("Id of the runner the call addressed."),
@@ -17758,10 +17836,11 @@ var makeGetRunnerContract = () => {
17758
17836
 
17759
17837
  // node_modules/@qawolf/api-contracts/dist/v1/runner/highlightSelector.js
17760
17838
  var maxHighlightSelectorLength = 2000;
17761
- var makeHighlightSelectorOnRunnerContract = () => {
17839
+ var makeHighlightSelectorOnRunnerContract = (ids) => {
17762
17840
  const input = object({
17763
17841
  id: runnerIdSchema.describe("Id of the runner to highlight on."),
17764
- selector: string2().max(maxHighlightSelectorLength).describe("The selector to highlight. An empty string clears whatever is highlighted.")
17842
+ selector: string2().max(maxHighlightSelectorLength).describe("The selector to highlight. An empty string clears whatever is highlighted."),
17843
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17765
17844
  });
17766
17845
  const output = discriminatedUnion("outcome", [
17767
17846
  object({
@@ -17796,12 +17875,13 @@ var makeHighlightSelectorOnRunnerContract = () => {
17796
17875
  // node_modules/@qawolf/api-contracts/dist/v1/runner/importPackage.js
17797
17876
  var maxPackageNameLength = 214;
17798
17877
  var maxPackageVersionLength = 256;
17799
- var makeImportPackageOnRunnerContract = () => {
17878
+ var makeImportPackageOnRunnerContract = (ids) => {
17800
17879
  const input = object({
17801
17880
  id: runnerIdSchema.describe("Id of the runner to install into."),
17802
17881
  npmDependencies: record(string2(), string2()).describe("The run's current npm dependencies, so the install resolves against them."),
17803
17882
  packageName: string2().min(1).max(maxPackageNameLength),
17804
- packageVersion: string2().min(1).max(maxPackageVersionLength)
17883
+ packageVersion: string2().min(1).max(maxPackageVersionLength),
17884
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17805
17885
  });
17806
17886
  const output = discriminatedUnion("outcome", [
17807
17887
  object({ outcome: literal("success") }),
@@ -17821,10 +17901,11 @@ var makeImportPackageOnRunnerContract = () => {
17821
17901
  };
17822
17902
 
17823
17903
  // node_modules/@qawolf/api-contracts/dist/v1/runner/index.js
17824
- var makeLaunchRunnerContract = () => {
17904
+ var makeLaunchRunnerContract = (ids) => {
17825
17905
  const input = object({
17826
17906
  id: runnerIdSchema.describe("Id to launch the runner under, chosen by the caller. Launching the same id again returns the runner already running under it."),
17827
- runnerName: runnerNameSchema.optional().describe("The runner family to launch. Defaults to the standard web family, playwright.")
17907
+ runnerName: runnerNameSchema.optional().describe("The runner family to launch. Defaults to the standard web family, playwright."),
17908
+ workspaceId: ids.workspace.optional().describe(`The workspace to launch the runner in. ${runnerWorkspaceRequirement}`)
17828
17909
  });
17829
17910
  const output = makeRunnerSchema().extend({
17830
17911
  alreadyRunning: boolean2().describe("True when a runner was already running under this id and was returned unchanged, so no second runner was started and nothing new is billed. Not an error either way."),
@@ -17838,9 +17919,10 @@ var makeLaunchRunnerContract = () => {
17838
17919
  output
17839
17920
  };
17840
17921
  };
17841
- var makeTerminateRunnerContract = () => {
17922
+ var makeTerminateRunnerContract = (ids) => {
17842
17923
  const input = object({
17843
- id: runnerIdSchema.describe("Id of the runner to terminate.")
17924
+ id: runnerIdSchema.describe("Id of the runner to terminate."),
17925
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17844
17926
  });
17845
17927
  const output = object({
17846
17928
  id: runnerIdSchema.describe("Id of the runner the call addressed."),
@@ -17857,14 +17939,16 @@ var makeTerminateRunnerContract = () => {
17857
17939
  };
17858
17940
 
17859
17941
  // node_modules/@qawolf/api-contracts/dist/v1/runner/list.js
17860
- var makeListRunnersContract = () => {
17861
- const input = object({});
17942
+ var makeListRunnersContract = (ids) => {
17943
+ const input = object({
17944
+ workspaceId: ids.workspace.optional().describe(`The workspace whose runners to list. ${runnerWorkspaceRequirement}`)
17945
+ });
17862
17946
  const output = object({
17863
17947
  outcome: literal("success"),
17864
17948
  runners: array(makeRunnerSchema()).describe("The runners running right now, in no particular order. Each entry carries the same fields a launch reports, so any of them can be addressed like a runner you launched yourself.")
17865
17949
  });
17866
17950
  return {
17867
- description: "List the runners running on the caller's team right now. Takes no arguments: the API key already names the team. A runner that was terminated, or that terminated on its own after inactivity, is not listed, because the run system keeps no record of a runner once it is gone. 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. It is how to find a runner launched from another machine or in an earlier session, when the id it was launched under is no longer at hand.",
17951
+ description: "List the runners running on the caller's team right now. A team API key already names the team and sends nothing; a credential bound to no single workspace names one in `workspaceId`. A runner that was terminated, or that terminated on its own after inactivity, is not listed, because the run system keeps no record of a runner once it is gone. 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. It is how to find a runner launched from another machine or in an earlier session, when the id it was launched under is no longer at hand.",
17868
17952
  input,
17869
17953
  kind: "read",
17870
17954
  name: "runner.list",
@@ -17874,11 +17958,12 @@ var makeListRunnersContract = () => {
17874
17958
 
17875
17959
  // node_modules/@qawolf/api-contracts/dist/v1/runner/promoteSnapshot.js
17876
17960
  var maxSnapshotPathLength = 1024;
17877
- var makePromoteSnapshotOnRunnerContract = () => {
17961
+ var makePromoteSnapshotOnRunnerContract = (ids) => {
17878
17962
  const input = object({
17879
17963
  baselinePath: string2().min(1).max(maxSnapshotPathLength).describe("The baseline to replace, as named in the image diff."),
17880
17964
  id: runnerIdSchema.describe("Id of the runner holding the screenshot."),
17881
- screenshotPath: string2().min(1).max(maxSnapshotPathLength).describe("The screenshot to promote, as named in the image diff.")
17965
+ screenshotPath: string2().min(1).max(maxSnapshotPathLength).describe("The screenshot to promote, as named in the image diff."),
17966
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17882
17967
  });
17883
17968
  const output = discriminatedUnion("outcome", [
17884
17969
  object({ outcome: literal("success") }),
@@ -17901,10 +17986,11 @@ var makePromoteSnapshotOnRunnerContract = () => {
17901
17986
  };
17902
17987
 
17903
17988
  // node_modules/@qawolf/api-contracts/dist/v1/runner/readJournal.js
17904
- var makeReadRunnerJournalContract = () => {
17989
+ var makeReadRunnerJournalContract = (ids) => {
17905
17990
  const input = readJournalRequestSchema.extend({
17906
17991
  id: runnerIdSchema.describe("Id of the runner to read the journal of."),
17907
- stream: journalStreamSchema.describe(`The stream to read. QA Wolf writes ${knownJournalStreams.join(", ")}; an unwritten stream reads as empty rather than as an error.`)
17992
+ stream: journalStreamSchema.describe(`The stream to read. QA Wolf writes ${knownJournalStreams.join(", ")}; an unwritten stream reads as empty rather than as an error.`),
17993
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17908
17994
  });
17909
17995
  const output = discriminatedUnion("outcome", [
17910
17996
  readJournalResponseSchema.extend({ outcome: literal("success") }),
@@ -17939,7 +18025,8 @@ var makeRunFlowOnRunnerContract = (ids) => {
17939
18025
  files: runFilesSchema.describe("Every file the run needs keyed by its path — the flow file, everything it imports, package.json and tsconfig.json. A runner holds no copy of your project, so what runs is exactly what is sent here."),
17940
18026
  id: runnerIdSchema.describe("Id of the runner to run the flow on."),
17941
18027
  selection: runSelectionSchema.optional().describe("Run only these lines, inside the page the runner is already on, instead of running the whole flow from a fresh browser. Omit to run the whole entry point."),
17942
- unchangedFiles: unchangedFilesSchema.optional().describe("Files this runner already holds from an earlier run, keyed by path with a hex SHA-256 of the content it should hold. Send this to make `files` only what changed. The runner refuses with `needs-full-sync` if it holds none of a referenced path, so it never runs something other than what was asked for. Omit it to send every file.")
18028
+ unchangedFiles: unchangedFilesSchema.optional().describe("Files this runner already holds from an earlier run, keyed by path with a hex SHA-256 of the content it should hold. Send this to make `files` only what changed. The runner refuses with `needs-full-sync` if it holds none of a referenced path, so it never runs something other than what was asked for. Omit it to send every file."),
18029
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17943
18030
  }).refine((request) => request.env === undefined || request.environmentId === undefined, {
17944
18031
  error: "Send env or environmentId, not both. An environment's variables are taken as they are stored, so there is no order in which the two would combine.",
17945
18032
  path: ["environmentId"]
@@ -17991,9 +18078,10 @@ var makeRunFlowOnRunnerContract = (ids) => {
17991
18078
  };
17992
18079
 
17993
18080
  // node_modules/@qawolf/api-contracts/dist/v1/runner/stopRun.js
17994
- var makeStopRunOnRunnerContract = () => {
18081
+ var makeStopRunOnRunnerContract = (ids) => {
17995
18082
  const input = object({
17996
- id: runnerIdSchema.describe("Id of the runner to stop the run on.")
18083
+ id: runnerIdSchema.describe("Id of the runner to stop the run on."),
18084
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
17997
18085
  });
17998
18086
  const output = discriminatedUnion("outcome", [
17999
18087
  object({
@@ -18012,13 +18100,14 @@ var makeStopRunOnRunnerContract = () => {
18012
18100
  };
18013
18101
 
18014
18102
  // node_modules/@qawolf/api-contracts/dist/v1/runner/takeScreenshot.js
18015
- var makeTakeScreenshotOnRunnerContract = () => {
18103
+ var makeTakeScreenshotOnRunnerContract = (ids) => {
18016
18104
  const input = object({
18017
- id: runnerIdSchema.describe("Id of the runner to take a screenshot of.")
18105
+ id: runnerIdSchema.describe("Id of the runner to take a screenshot of."),
18106
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
18018
18107
  });
18019
18108
  const output = discriminatedUnion("outcome", [
18020
18109
  object({
18021
- imageJpegBase64: string2().min(1).describe("The screenshot, as a base64-encoded JPEG. Decode it and write the bytes to a `.jpg` file — writing this string to the file leaves you with base64 text rather than an image."),
18110
+ imageJpegBase64: screenshotImageSchema,
18022
18111
  outcome: literal("success")
18023
18112
  }),
18024
18113
  makeRunnerFailureSchema([
@@ -18072,6 +18161,7 @@ var makeContractsV1 = (ids) => {
18072
18161
  get: makeGetEmailContract(resolvedIds),
18073
18162
  getAttachment: makeGetEmailAttachmentContract(resolvedIds),
18074
18163
  listAddresses: makeListEmailAddressesContract(resolvedIds),
18164
+ registerAddress: makeRegisterEmailAddressContract(resolvedIds),
18075
18165
  send: makeSendEmailContract(resolvedIds)
18076
18166
  },
18077
18167
  environment: {
@@ -18103,24 +18193,25 @@ var makeContractsV1 = (ids) => {
18103
18193
  diagnose: makeDiagnoseRunContract(resolvedIds),
18104
18194
  find: makeFindRunsContract(resolvedIds),
18105
18195
  get: makeGetRunContract(resolvedIds),
18106
- reattempt: makeReattemptRunContract(resolvedIds)
18196
+ reattempt: makeReattemptRunContract(resolvedIds),
18197
+ stop: makeStopRunContract(resolvedIds)
18107
18198
  },
18108
18199
  runner: {
18109
- evaluateSnippet: makeEvaluateSnippetOnRunnerContract(),
18110
- get: makeGetRunnerContract(),
18111
- highlightSelector: makeHighlightSelectorOnRunnerContract(),
18112
- importPackage: makeImportPackageOnRunnerContract(),
18113
- inspect: makeInspectOnRunnerContract(),
18114
- inspectMobile: makeInspectMobileOnRunnerContract(),
18115
- launch: makeLaunchRunnerContract(),
18116
- list: makeListRunnersContract(),
18117
- performAction: makePerformActionOnRunnerContract(),
18118
- promoteSnapshot: makePromoteSnapshotOnRunnerContract(),
18119
- readJournal: makeReadRunnerJournalContract(),
18200
+ evaluateSnippet: makeEvaluateSnippetOnRunnerContract(resolvedIds),
18201
+ get: makeGetRunnerContract(resolvedIds),
18202
+ highlightSelector: makeHighlightSelectorOnRunnerContract(resolvedIds),
18203
+ importPackage: makeImportPackageOnRunnerContract(resolvedIds),
18204
+ inspect: makeInspectOnRunnerContract(resolvedIds),
18205
+ inspectMobile: makeInspectMobileOnRunnerContract(resolvedIds),
18206
+ launch: makeLaunchRunnerContract(resolvedIds),
18207
+ list: makeListRunnersContract(resolvedIds),
18208
+ performAction: makePerformActionOnRunnerContract(resolvedIds),
18209
+ promoteSnapshot: makePromoteSnapshotOnRunnerContract(resolvedIds),
18210
+ readJournal: makeReadRunnerJournalContract(resolvedIds),
18120
18211
  runFlow: makeRunFlowOnRunnerContract(resolvedIds),
18121
- stopRun: makeStopRunOnRunnerContract(),
18122
- takeScreenshot: makeTakeScreenshotOnRunnerContract(),
18123
- terminate: makeTerminateRunnerContract()
18212
+ stopRun: makeStopRunOnRunnerContract(resolvedIds),
18213
+ takeScreenshot: makeTakeScreenshotOnRunnerContract(resolvedIds),
18214
+ terminate: makeTerminateRunnerContract(resolvedIds)
18124
18215
  },
18125
18216
  tag: {
18126
18217
  create: makeCreateTagContract(resolvedIds),
@@ -18523,6 +18614,34 @@ async function writeTeamStorageAssets(args, deps) {
18523
18614
  }
18524
18615
  }
18525
18616
 
18617
+ // src/shell/platform/teamStorageMethods.ts
18618
+ function createTeamStorageMethods(trpc, deps, fs, getIdentity) {
18619
+ async function list() {
18620
+ if (deps.workspaceId !== undefined) {
18621
+ return listTeamStorageFiles(trpc, { teamId: deps.workspaceId }, deps);
18622
+ }
18623
+ const identity = await getIdentity();
18624
+ if (!identity.ok)
18625
+ return identity;
18626
+ if (!("team" in identity.value)) {
18627
+ return {
18628
+ ok: false,
18629
+ error: flowsMessages.pull.teamStorageRequiresTeamKey
18630
+ };
18631
+ }
18632
+ return listTeamStorageFiles(trpc, { teamId: identity.value.team.id }, deps);
18633
+ }
18634
+ return {
18635
+ listTeamStorageFiles: list,
18636
+ async syncTeamStorageAssets(assetsAbs, opts) {
18637
+ const files = await list();
18638
+ if (!files.ok)
18639
+ return files;
18640
+ return downloadTeamStorageAssets({ assetsAbs, files: files.value }, { fetch: deps.fetch, fs, onProgress: opts?.onProgress });
18641
+ }
18642
+ };
18643
+ }
18644
+
18526
18645
  // src/shell/platform/createPlatformClient.ts
18527
18646
  var requestBackoffMs2 = [500, 1500];
18528
18647
  function createPlatformClient(apiKey, deps) {
@@ -18539,8 +18658,10 @@ function createPlatformClient(apiKey, deps) {
18539
18658
  return result;
18540
18659
  return { ok: true, value: { signedUrl: result.value.url } };
18541
18660
  }
18661
+ const identityMethods = createIdentityMethods(apiKey, deps, requestBackoffMs2);
18542
18662
  return {
18543
- ...createIdentityMethods(apiKey, deps, requestBackoffMs2),
18663
+ ...identityMethods,
18664
+ ...createTeamStorageMethods(trpc, deps, fs, identityMethods.getIdentity),
18544
18665
  getFlowsBundleUrl: getFlowsBundleUrlImpl,
18545
18666
  callPublicApi: makeCallPublicApiMethod(trpc, deps, requestBackoffMs2),
18546
18667
  async getEnvVars(envId) {
@@ -18554,24 +18675,6 @@ function createPlatformClient(apiKey, deps) {
18554
18675
  return result;
18555
18676
  return { ok: true, value: result.value.environmentVariables };
18556
18677
  },
18557
- async listTeamStorageFiles() {
18558
- const identity = await this.getIdentity();
18559
- if (!identity.ok)
18560
- return identity;
18561
- if (!("team" in identity.value)) {
18562
- return {
18563
- ok: false,
18564
- error: flowsMessages.pull.teamStorageRequiresTeamKey
18565
- };
18566
- }
18567
- return listTeamStorageFiles(trpc, { teamId: identity.value.team.id }, deps);
18568
- },
18569
- async syncTeamStorageAssets(assetsAbs, opts) {
18570
- const files = await this.listTeamStorageFiles();
18571
- if (!files.ok)
18572
- return files;
18573
- return downloadTeamStorageAssets({ assetsAbs, files: files.value }, { fetch: deps.fetch, fs, onProgress: opts?.onProgress });
18574
- },
18575
18678
  async downloadBundle(envId) {
18576
18679
  const urlResult = await getFlowsBundleUrlImpl(envId);
18577
18680
  if (!urlResult.ok)
@@ -20372,9 +20475,11 @@ var registryUrl = "https://registry.npmjs.org";
20372
20475
  var timeoutMs4 = 3000;
20373
20476
  async function fetchLatestVersion(packageName, deps = {}) {
20374
20477
  const fetchFn = deps.fetchFn ?? globalThis.fetch;
20478
+ const deadline = AbortSignal.timeout(timeoutMs4);
20479
+ const signal = deps.signal ? AbortSignal.any([deps.signal, deadline]) : deadline;
20375
20480
  try {
20376
20481
  const response = await fetchFn(`${registryUrl}/${packageName}/latest`, {
20377
- signal: AbortSignal.timeout(timeoutMs4)
20482
+ signal
20378
20483
  });
20379
20484
  if (!response.ok)
20380
20485
  return;
@@ -20423,14 +20528,16 @@ function startUpdateCheck(deps) {
20423
20528
  if (deps.env["QAWOLF_NO_UPDATE_CHECK"]) {
20424
20529
  return noopNotifier;
20425
20530
  }
20531
+ const controller = new AbortController;
20426
20532
  let latest;
20427
- deps.fetchLatestVersion().then((version) => {
20533
+ deps.fetchLatestVersion(controller.signal).then((version) => {
20428
20534
  latest = version;
20429
20535
  }, () => {
20430
20536
  return;
20431
20537
  });
20432
20538
  return {
20433
20539
  async notifyIfOutdated() {
20540
+ controller.abort();
20434
20541
  if (latest === undefined)
20435
20542
  return;
20436
20543
  if (!isNewerVersion(deps.currentVersion, latest))
@@ -20457,7 +20564,7 @@ function startUpdateCheck(deps) {
20457
20564
  // package.json
20458
20565
  var package_default = {
20459
20566
  name: "@qawolf/cli",
20460
- version: "1.25.0",
20567
+ version: "1.27.0",
20461
20568
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
20462
20569
  keywords: [
20463
20570
  "automation",
@@ -20528,7 +20635,7 @@ var package_default = {
20528
20635
  "@clack/prompts": "1.5.1",
20529
20636
  "@napi-rs/keyring": "1.3.0",
20530
20637
  "@oxc-node/core": "0.1.0",
20531
- "@qawolf/api-contracts": "0.47.0",
20638
+ "@qawolf/api-contracts": "0.52.0",
20532
20639
  "@qawolf/emails": "1.1.1",
20533
20640
  "@qawolf/flow-targets": "1.0.0",
20534
20641
  "@qawolf/flows": "0.1.4",
@@ -20594,7 +20701,7 @@ function buildBaseContext(command, signals) {
20594
20701
  currentVersion: package_default.version,
20595
20702
  configDir: getConfigDir(),
20596
20703
  fs,
20597
- fetchLatestVersion: () => fetchLatestVersion(package_default.name),
20704
+ fetchLatestVersion: (signal) => fetchLatestVersion(package_default.name, { signal }),
20598
20705
  renderNotice: (body, title) => ui.note(body, title)
20599
20706
  });
20600
20707
  return {
@@ -32214,6 +32321,28 @@ function registerPublicApiCommands(program, signals, options = {}) {
32214
32321
  }
32215
32322
  }
32216
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
+
32217
32346
  // src/domains/interactiveRunner/runnerCallOptions.ts
32218
32347
  var runnerCallOptions = { timeoutMs: 90000 };
32219
32348
 
@@ -32326,134 +32455,205 @@ function announceRunner(ctx, resolved) {
32326
32455
  }
32327
32456
  }
32328
32457
 
32329
- // src/domains/interactiveRunner/highlightSelector.ts
32330
- async function handleRunnerHighlightSelector(ctx, options, deps) {
32331
- const resolved = await resolveRunner(ctx, {
32332
- autoLaunch: false,
32333
- noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForHighlight,
32334
- runner: options.runner
32335
- }, deps);
32336
- if (resolved.type === "failed") {
32337
- return { ...failureFields(resolved), exitCode: resolved.exitCode };
32338
- }
32339
- const selector = options.selector ?? "";
32340
- const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.highlightSelector, { id: resolved.runnerId, selector }, runnerCallOptions);
32341
- if (!result.ok) {
32342
- return { ...failureFields(result), exitCode: exitCodes.network };
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" };
32343
32474
  }
32344
- const { outcome } = result.value;
32345
- switch (outcome) {
32346
- case "cleared":
32347
- ctx.ui.output(result.value, interactiveRunnerMessages.highlightCleared);
32348
- return;
32349
- case "success": {
32350
- const answer = result.value;
32351
- if (answer.status === "invalid") {
32352
- return {
32353
- error: interactiveRunnerMessages.highlightSelectorInvalid(answer.selector),
32354
- exitCode: exitCodes.invalidArgs
32355
- };
32356
- }
32357
- ctx.ui.output(answer, answer.status === "empty" ? interactiveRunnerMessages.highlightMatchedNothing(answer.selector) : interactiveRunnerMessages.highlightMatched(answer.matchCount, answer.targetPage));
32358
- return;
32359
- }
32360
- case "failure":
32361
- return describeFailure(result.value.failureReason);
32362
- default:
32363
- return {
32364
- error: interactiveRunnerMessages.highlightAnsweredUnknown(outcome),
32365
- exitCode: exitCodes.network
32366
- };
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
+ };
32367
32483
  }
32368
- }
32369
- function describeFailure(failureReason) {
32370
- switch (failureReason) {
32371
- case "no-answer":
32372
- return {
32373
- error: interactiveRunnerMessages.highlightNoAnswer,
32374
- exitCode: exitCodes.network
32375
- };
32376
- case "runner-cannot-highlight-selectors":
32377
- return {
32378
- error: interactiveRunnerMessages.runnerCannotHighlightSelectors,
32379
- exitCode: exitCodes.invalidArgs
32380
- };
32381
- case "runner-unreachable":
32382
- return {
32383
- error: interactiveRunnerMessages.runnerUnreachable,
32384
- exitCode: exitCodes.network
32385
- };
32386
- default:
32387
- return {
32388
- error: interactiveRunnerMessages.highlightAnsweredUnknown(failureReason),
32389
- exitCode: exitCodes.network
32390
- };
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
+ };
32391
32491
  }
32492
+ return { type: "ok" };
32392
32493
  }
32393
-
32394
- // src/shell/interactiveRunner/collectRunFiles.ts
32395
- import { join as join51 } from "node:path";
32396
-
32397
- // src/core/interactiveRunner/getImports.ts
32398
- function getImports(options) {
32399
- const { typescript: compiler } = options;
32400
- const sourceFile = compiler.createSourceFile(options.path, options.content, compiler.ScriptTarget.Latest, true);
32401
- const isLocal = (importPath) => importPath.startsWith("./") || importPath.startsWith("../") || isPathAlias(importPath, options.tsconfigPaths);
32402
- const found = [];
32403
- for (const statement of sourceFile.statements) {
32404
- if (!compiler.isImportDeclaration(statement))
32405
- continue;
32406
- const { moduleSpecifier } = statement;
32407
- if (!compiler.isStringLiteral(moduleSpecifier))
32408
- continue;
32409
- if (isLocal(moduleSpecifier.text))
32410
- found.push(moduleSpecifier.text);
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" };
32411
32500
  }
32412
- const visit = (node) => {
32413
- if (isDynamicImportCall(node, compiler)) {
32414
- const [argument] = node.arguments;
32415
- if (argument !== undefined && compiler.isStringLiteral(argument) && isLocal(argument.text)) {
32416
- found.push(argument.text);
32417
- }
32418
- }
32419
- compiler.forEachChild(node, visit);
32420
- };
32421
- visit(sourceFile);
32422
- return [...new Set(found)];
32501
+ return { type: "ok" };
32423
32502
  }
32424
- function isDynamicImportCall(node, compiler) {
32425
- return compiler.isCallExpression(node) && node.expression.kind === compiler.SyntaxKind.ImportKeyword;
32503
+ function checkSnippetFiles(files, contextFilePath) {
32504
+ return checkPresenceAndSize(files, contextFilePath);
32426
32505
  }
32427
- function isPathAlias(importPath, paths) {
32428
- if (paths === undefined)
32429
- return false;
32430
- return Object.keys(paths).some((pattern) => importPath.startsWith(pattern.replace("*", "")));
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
+ }
32431
32517
  }
32432
32518
 
32433
- // src/core/interactiveRunner/resolveImportPath.ts
32434
- import { dirname as dirname15, join as join50, normalize as normalize3 } from "node:path/posix";
32435
-
32436
- // src/core/interactiveRunner/tsconfigPaths.ts
32437
- function isRecord4(value) {
32438
- return typeof value === "object" && value !== null;
32439
- }
32440
- function isTsconfigPaths(value) {
32441
- return isRecord4(value) && Object.values(value).every((targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string"));
32442
- }
32443
- function parseTsconfigPaths(tsconfigContent) {
32444
- let parsed;
32445
- try {
32446
- parsed = JSON.parse(tsconfigContent);
32447
- } catch {
32448
- return;
32519
+ // src/domains/interactiveRunner/snippetScope.ts
32520
+ async function resolveSnippetScope(contextFile, deps) {
32521
+ if (contextFile === undefined) {
32522
+ return { filePath: undefined, files: undefined, ok: true };
32449
32523
  }
32450
- if (!isRecord4(parsed))
32451
- return;
32452
- const compilerOptions = parsed["compilerOptions"];
32453
- if (!isRecord4(compilerOptions))
32454
- return;
32455
- const paths = compilerOptions["paths"];
32456
- return isTsconfigPaths(paths) ? paths : undefined;
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);
32558
+ if (resolved.type === "failed") {
32559
+ return { ...failureFields(resolved), exitCode: resolved.exitCode };
32560
+ }
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);
32568
+ if (!result.ok) {
32569
+ return {
32570
+ ...failureFields(result),
32571
+ exitCode: result.exitCode ?? exitCodes.network
32572
+ };
32573
+ }
32574
+ if (result.value.outcome === "failure") {
32575
+ return describeEvaluateSnippetFailure(result.value);
32576
+ }
32577
+ switch (result.value.result) {
32578
+ case "success":
32579
+ ctx.ui.output({ outcome: "evaluated", result: "success" }, interactiveRunnerMessages.snippetRan);
32580
+ return;
32581
+ case "stopped":
32582
+ return {
32583
+ error: interactiveRunnerMessages.snippetStopped,
32584
+ exitCode: exitCodes.testFailure
32585
+ };
32586
+ case "error":
32587
+ return {
32588
+ error: interactiveRunnerMessages.snippetErrored(result.value.errorMessage),
32589
+ exitCode: exitCodes.testFailure
32590
+ };
32591
+ }
32592
+ }
32593
+
32594
+ // src/shell/interactiveRunner/collectRunFiles.ts
32595
+ import { join as join51 } from "node:path";
32596
+
32597
+ // src/core/interactiveRunner/getImports.ts
32598
+ function getImports(options) {
32599
+ const { typescript: compiler } = options;
32600
+ const sourceFile = compiler.createSourceFile(options.path, options.content, compiler.ScriptTarget.Latest, true);
32601
+ const isLocal = (importPath) => importPath.startsWith("./") || importPath.startsWith("../") || isPathAlias(importPath, options.tsconfigPaths);
32602
+ const found = [];
32603
+ for (const statement of sourceFile.statements) {
32604
+ if (!compiler.isImportDeclaration(statement))
32605
+ continue;
32606
+ const { moduleSpecifier } = statement;
32607
+ if (!compiler.isStringLiteral(moduleSpecifier))
32608
+ continue;
32609
+ if (isLocal(moduleSpecifier.text))
32610
+ found.push(moduleSpecifier.text);
32611
+ }
32612
+ const visit = (node) => {
32613
+ if (isDynamicImportCall(node, compiler)) {
32614
+ const [argument] = node.arguments;
32615
+ if (argument !== undefined && compiler.isStringLiteral(argument) && isLocal(argument.text)) {
32616
+ found.push(argument.text);
32617
+ }
32618
+ }
32619
+ compiler.forEachChild(node, visit);
32620
+ };
32621
+ visit(sourceFile);
32622
+ return [...new Set(found)];
32623
+ }
32624
+ function isDynamicImportCall(node, compiler) {
32625
+ return compiler.isCallExpression(node) && node.expression.kind === compiler.SyntaxKind.ImportKeyword;
32626
+ }
32627
+ function isPathAlias(importPath, paths) {
32628
+ if (paths === undefined)
32629
+ return false;
32630
+ return Object.keys(paths).some((pattern) => importPath.startsWith(pattern.replace("*", "")));
32631
+ }
32632
+
32633
+ // src/core/interactiveRunner/resolveImportPath.ts
32634
+ import { dirname as dirname15, join as join50, normalize as normalize3 } from "node:path/posix";
32635
+
32636
+ // src/core/interactiveRunner/tsconfigPaths.ts
32637
+ function isRecord4(value) {
32638
+ return typeof value === "object" && value !== null;
32639
+ }
32640
+ function isTsconfigPaths(value) {
32641
+ return isRecord4(value) && Object.values(value).every((targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string"));
32642
+ }
32643
+ function parseTsconfigPaths(tsconfigContent) {
32644
+ let parsed;
32645
+ try {
32646
+ parsed = JSON.parse(tsconfigContent);
32647
+ } catch {
32648
+ return;
32649
+ }
32650
+ if (!isRecord4(parsed))
32651
+ return;
32652
+ const compilerOptions = parsed["compilerOptions"];
32653
+ if (!isRecord4(compilerOptions))
32654
+ return;
32655
+ const paths = compilerOptions["paths"];
32656
+ return isTsconfigPaths(paths) ? paths : undefined;
32457
32657
  }
32458
32658
  function resolvePathAlias(importPath, paths) {
32459
32659
  if (paths === undefined)
@@ -32729,19 +32929,38 @@ function makeRunnerStore(options) {
32729
32929
  // src/shell/interactiveRunner/writeScreenshot.ts
32730
32930
  import { dirname as dirname16 } from "node:path";
32731
32931
  var jpegStartOfImage = [255, 216, 255];
32932
+ var stdoutPath = "-";
32732
32933
  async function writeScreenshot(options) {
32733
32934
  const bytes = Buffer.from(options.imageJpegBase64, "base64");
32734
32935
  if (jpegStartOfImage.some((byte, index) => bytes[index] !== byte)) {
32735
32936
  return { ok: false, reason: "not-a-jpeg" };
32736
32937
  }
32737
32938
  try {
32738
- await options.fs.mkdir(dirname16(options.path), { recursive: true });
32739
- 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
+ }
32740
32945
  } catch (error) {
32741
32946
  return { detail: errorMessage(error), ok: false, reason: "unwritable" };
32742
32947
  }
32743
32948
  return { ok: true };
32744
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
+ }
32745
32964
 
32746
32965
  // src/shell/stdin.ts
32747
32966
  async function readStdin2() {
@@ -32782,6 +33001,81 @@ function runnerDeps(ctx) {
32782
33001
  });
32783
33002
  }
32784
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
+
32785
33079
  // src/commands/runner/highlightSelector.register.ts
32786
33080
  var highlightExamples = `
32787
33081
  Examples:
@@ -33171,162 +33465,10 @@ function registerRunnerInspectCommands(runner, signals) {
33171
33465
  registerRunnerInspectMobileCommands(inspect, signals);
33172
33466
  }
33173
33467
 
33174
- // src/domains/interactiveRunner/evaluateSnippetFailure.ts
33175
- function describeEvaluateSnippetFailure(failure) {
33176
- const { failureReason } = failure;
33177
- switch (failureReason) {
33178
- case "runner-cannot-evaluate-snippets":
33179
- return {
33180
- error: interactiveRunnerMessages.runnerCannotEvaluateSnippets,
33181
- exitCode: exitCodes.invalidArgs
33182
- };
33183
- case "runner-unreachable":
33184
- return {
33185
- error: interactiveRunnerMessages.snippetRunnerUnreachable,
33186
- exitCode: exitCodes.network
33187
- };
33188
- default:
33189
- return {
33190
- error: interactiveRunnerMessages.snippetAnsweredUnknown(failureReason),
33191
- exitCode: exitCodes.network
33192
- };
33193
- }
33194
- }
33195
-
33196
- // src/core/interactiveRunner/runFiles.ts
33197
- import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve11, sep as sep5 } from "node:path";
33198
- function toCollectedPath(cwd, path) {
33199
- const absolute = isAbsolute5(path) ? path : resolve11(cwd, path);
33200
- return relative10(cwd, absolute).split(sep5).join("/");
33201
- }
33202
- var namedLargestFileCount = 3;
33203
- function largestFiles(files) {
33204
- return Object.entries(files).map(([path, content]) => ({
33205
- byteLength: runFilesByteLength({ [path]: content }),
33206
- path
33207
- })).sort((a, b) => b.byteLength - a.byteLength).slice(0, namedLargestFileCount);
33208
- }
33209
- function checkPresenceAndSize(files, requiredPath) {
33210
- if (!Object.hasOwn(files, requiredPath)) {
33211
- return { path: requiredPath, type: "missing-file" };
33212
- }
33213
- const byteLength = runFilesByteLength(files);
33214
- if (byteLength > maxRunFilesByteLength) {
33215
- return {
33216
- byteLength,
33217
- largest: largestFiles(files),
33218
- maxByteLength: maxRunFilesByteLength,
33219
- type: "too-large"
33220
- };
33221
- }
33222
- const encodedByteLength = Buffer.byteLength(JSON.stringify({ files, path: requiredPath }), "utf8");
33223
- if (encodedByteLength > maxRunnerRequestEncodedByteLength) {
33224
- return {
33225
- byteLength: encodedByteLength,
33226
- maxByteLength: maxRunnerRequestEncodedByteLength,
33227
- type: "request-too-large"
33228
- };
33229
- }
33230
- return { type: "ok" };
33231
- }
33232
- function checkRunFiles(files, entryPointPath) {
33233
- const present = checkPresenceAndSize(files, entryPointPath);
33234
- if (present.type !== "ok")
33235
- return present;
33236
- if (!Object.hasOwn(files, runPackageJsonPath)) {
33237
- return { type: "missing-package-json" };
33238
- }
33239
- return { type: "ok" };
33240
- }
33241
- function checkSnippetFiles(files, contextFilePath) {
33242
- return checkPresenceAndSize(files, contextFilePath);
33243
- }
33244
- function describeRunFilesCheck(check) {
33245
- switch (check.type) {
33246
- case "missing-file":
33247
- return interactiveRunnerMessages.fileNotCollected(check.path);
33248
- case "missing-package-json":
33249
- return interactiveRunnerMessages.missingPackageJson;
33250
- case "too-large":
33251
- return interactiveRunnerMessages.filesTooLarge(check.byteLength, check.maxByteLength, check.largest);
33252
- case "request-too-large":
33253
- return interactiveRunnerMessages.requestTooLarge(check.byteLength, check.maxByteLength);
33254
- }
33255
- }
33256
-
33257
- // src/domains/interactiveRunner/snippetScope.ts
33258
- async function resolveSnippetScope(contextFile, deps) {
33259
- if (contextFile === undefined) {
33260
- return { filePath: undefined, files: undefined, ok: true };
33261
- }
33262
- const filePath = toCollectedPath(deps.cwd, contextFile);
33263
- const { files } = await deps.collectRunFiles([filePath]);
33264
- const check = checkSnippetFiles(files, filePath);
33265
- if (check.type !== "ok") {
33266
- return { error: describeRunFilesCheck(check), ok: false };
33267
- }
33268
- return { filePath, files, ok: true };
33269
- }
33270
-
33271
- // src/domains/interactiveRunner/evaluateSnippet.ts
33272
- var stdinArgument = "-";
33273
- async function readCode(source, deps) {
33274
- const code = source === stdinArgument ? await deps.readStdin() : await deps.readFile(source).catch(() => {
33275
- return;
33276
- });
33277
- if (code === undefined) {
33278
- return {
33279
- error: interactiveRunnerMessages.snippetFileUnreadable(source),
33280
- ok: false
33281
- };
33282
- }
33283
- if (code.trim() === "") {
33284
- return source === stdinArgument ? { error: interactiveRunnerMessages.stdinEmptySnippet, ok: false } : { error: interactiveRunnerMessages.snippetEmpty(source), ok: false };
33285
- }
33286
- return { code, ok: true };
33287
- }
33288
- async function handleRunnerExec(ctx, options, deps) {
33289
- const code = await readCode(options.source, deps);
33290
- if (!code.ok)
33291
- return { error: code.error, exitCode: exitCodes.invalidArgs };
33292
- const scope = await resolveSnippetScope(options.contextFile, deps);
33293
- if (!scope.ok)
33294
- return { error: scope.error, exitCode: exitCodes.invalidArgs };
33295
- const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
33296
- if (resolved.type === "failed") {
33297
- return { ...failureFields(resolved), exitCode: resolved.exitCode };
33298
- }
33299
- announceRunner(ctx, resolved);
33300
- const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.evaluateSnippet, {
33301
- code: code.code,
33302
- id: resolved.runnerId,
33303
- ...scope.filePath === undefined ? {} : { filePath: scope.filePath },
33304
- ...scope.files === undefined ? {} : { files: scope.files }
33305
- }, runnerCallOptions);
33306
- if (!result.ok) {
33307
- return {
33308
- ...failureFields(result),
33309
- exitCode: result.exitCode ?? exitCodes.network
33310
- };
33311
- }
33312
- if (result.value.outcome === "failure") {
33313
- return describeEvaluateSnippetFailure(result.value);
33314
- }
33315
- switch (result.value.result) {
33316
- case "success":
33317
- ctx.ui.output({ outcome: "evaluated", result: "success" }, interactiveRunnerMessages.snippetRan);
33318
- return;
33319
- case "stopped":
33320
- return {
33321
- error: interactiveRunnerMessages.snippetStopped,
33322
- exitCode: exitCodes.testFailure
33323
- };
33324
- case "error":
33325
- return {
33326
- error: interactiveRunnerMessages.snippetErrored(result.value.errorMessage),
33327
- exitCode: exitCodes.testFailure
33328
- };
33329
- }
33468
+ // src/core/sentences.ts
33469
+ function appendSentence(text, sentence) {
33470
+ const punctuated = /[.!?]$/.test(text.trimEnd()) ? text.trimEnd() : `${text.trimEnd()}.`;
33471
+ return `${punctuated} ${sentence}`;
33330
33472
  }
33331
33473
 
33332
33474
  // src/domains/interactiveRunner/performActionFailure.ts
@@ -33373,6 +33515,56 @@ function describePerformActionFailure(options) {
33373
33515
  }
33374
33516
  }
33375
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
+
33376
33568
  // src/core/interactiveRunner/browserAction.ts
33377
33569
  function parseJsonPath(path) {
33378
33570
  try {
@@ -33437,30 +33629,56 @@ async function handleRunnerAct(ctx, options, deps) {
33437
33629
  const built = await readAction(options.type, options.flags, deps);
33438
33630
  if (!built.ok)
33439
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
+ }
33440
33638
  const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
33441
33639
  if (resolved.type === "failed") {
33442
33640
  return { ...failureFields(resolved), exitCode: resolved.exitCode };
33443
33641
  }
33444
33642
  announceRunner(ctx, resolved);
33445
- 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);
33446
33648
  if (!result.ok) {
33447
33649
  const fields = failureFields(result);
33448
33650
  return {
33449
33651
  ...fields,
33450
33652
  ...result.mayHaveArrived ? {
33451
- error: `${fields.error} ${interactiveRunnerMessages.actionMayHaveHappened}`
33653
+ error: appendSentence(fields.error, interactiveRunnerMessages.actionMayHaveHappened)
33452
33654
  } : {},
33453
33655
  exitCode: exitCodes.network
33454
33656
  };
33455
33657
  }
33456
- 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
+ }
33457
33667
  ctx.ui.output({ action: built.action, outcome: "success" }, interactiveRunnerMessages.actionPerformed(built.action.type));
33458
33668
  return;
33459
33669
  }
33460
- return describePerformActionFailure({
33670
+ const failure = describePerformActionFailure({
33461
33671
  actionType: built.action.type,
33462
- failure: result.value
33672
+ failure: answer
33463
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);
33464
33682
  }
33465
33683
 
33466
33684
  // src/domains/interactiveRunner/takeScreenshot.ts
@@ -33473,6 +33691,12 @@ async function handleRunnerScreenshot(ctx, options, deps) {
33473
33691
  if (resolved.type === "failed") {
33474
33692
  return { ...failureFields(resolved), exitCode: resolved.exitCode };
33475
33693
  }
33694
+ if (options.out === stdoutPath && ctx.outputMode === "human") {
33695
+ return {
33696
+ error: interactiveRunnerMessages.stdoutIsATerminal("--out"),
33697
+ exitCode: exitCodes.invalidArgs
33698
+ };
33699
+ }
33476
33700
  const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.takeScreenshot, { id: resolved.runnerId }, runnerCallOptions);
33477
33701
  if (!result.ok) {
33478
33702
  return { ...failureFields(result), exitCode: exitCodes.network };
@@ -33482,16 +33706,13 @@ async function handleRunnerScreenshot(ctx, options, deps) {
33482
33706
  imageJpegBase64: result.value.imageJpegBase64,
33483
33707
  path: options.out
33484
33708
  });
33485
- if (!written.ok) {
33486
- return written.reason === "not-a-jpeg" ? {
33487
- error: interactiveRunnerMessages.screenshotNotAnImage,
33488
- exitCode: exitCodes.network
33489
- } : {
33490
- error: interactiveRunnerMessages.screenshotUnwritable(options.out, written.detail),
33491
- exitCode: exitCodes.invalidArgs
33492
- };
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));
33493
33715
  }
33494
- ctx.ui.output({ outcome: "success", path: options.out }, interactiveRunnerMessages.screenshotWritten(options.out));
33495
33716
  return;
33496
33717
  }
33497
33718
  const { failureReason } = result.value;
@@ -33524,13 +33745,27 @@ async function handleRunnerScreenshot(ctx, options, deps) {
33524
33745
  }
33525
33746
  }
33526
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
+ }
33527
33760
 
33528
33761
  // src/commands/runner/interact.register.ts
33529
33762
  var defaultScreenshotPath = "screenshot.jpg";
33530
33763
  var screenshotExamples = `
33531
33764
  Examples:
33532
33765
  $ qawolf runner screenshot
33533
- $ 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`;
33534
33769
  var actExamples = `
33535
33770
  Examples:
33536
33771
  $ qawolf runner act click --button left --x 480 --y 260
@@ -33538,15 +33773,12 @@ Examples:
33538
33773
  $ qawolf runner act keypress --keys Control a
33539
33774
  $ qawolf runner act navigate --url https://example.com
33540
33775
  $ qawolf runner act drag --path '[{"x":10,"y":20},{"x":80,"y":90}]'
33541
- $ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act -`;
33542
- var execExamples = `
33543
- Examples:
33544
- $ qawolf runner exec snippet.ts
33545
- $ echo 'console.log(await page.title())' | qawolf runner exec -
33546
- $ 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`;
33547
33779
  function registerRunnerInteractCommands(runner, signals) {
33548
- 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));
33549
- 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, {
33550
33782
  flags: {
33551
33783
  button: opts.button,
33552
33784
  keys: opts.keys,
@@ -33559,9 +33791,9 @@ function registerRunnerInteractCommands(runner, signals) {
33559
33791
  y: opts.y
33560
33792
  },
33561
33793
  runner: opts.runner,
33794
+ screenshot: opts.screenshot,
33562
33795
  type: action
33563
33796
  }, runnerDeps(ctx)))(opts, command));
33564
- 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));
33565
33797
  }
33566
33798
 
33567
33799
  // src/domains/interactiveRunner/readJournal.ts
@@ -34516,6 +34748,7 @@ function registerRunnerCommand(program, signals) {
34516
34748
  registerRunCommand(runner, signals);
34517
34749
  registerRunnerEventsCommand(runner, signals);
34518
34750
  registerRunnerInteractCommands(runner, signals);
34751
+ registerRunnerExecCommand(runner, signals);
34519
34752
  registerRunnerInspectCommands(runner, signals);
34520
34753
  registerRunnerImportPackageCommand(runner, signals);
34521
34754
  registerRunnerHighlightSelectorCommand(runner, signals);
@@ -34541,21 +34774,12 @@ function createProgram({
34541
34774
 
34542
34775
  // src/main.ts
34543
34776
  var signals = createSignalRegistry();
34544
- var forced = false;
34545
- var onSignal = (sig) => () => {
34546
- const code = sig === "SIGINT" ? 130 : 143;
34547
- if (forced)
34548
- process.exit(code);
34549
- forced = true;
34550
- signals.shutdown(sig).finally(() => {
34551
- process.exit(code);
34552
- });
34553
- };
34777
+ var onSignal = createSignalExit({ shutdown: (sig) => signals.shutdown(sig) });
34554
34778
  process.on("SIGINT", onSignal("SIGINT"));
34555
34779
  process.on("SIGTERM", onSignal("SIGTERM"));
34556
34780
  createProgram({ signals }).parseAsync().catch(() => {
34557
34781
  if (process.exitCode === undefined)
34558
34782
  process.exitCode = 1;
34559
- }).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
34783
+ }).finally(() => exitWhenIdle(typeof process.exitCode === "number" ? process.exitCode : 0));
34560
34784
 
34561
- //# debugId=DA8D4A9E373CFE5C64756E2164756E21
34785
+ //# debugId=90417CF807BBB8C264756E2164756E21