@qawolf/cli 1.8.1 → 1.9.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
@@ -156391,6 +156391,60 @@ var installMessages = {
156391
156391
  appiumNotFound: (appiumPath) => toolNotInstalled("Appium", appiumPath)
156392
156392
  }
156393
156393
  };
156394
+ // src/core/messages/interactiveRunner.ts
156395
+ var noRunnerId = "No runner id. Pass --runner, set QAWOLF_RUNNER_ID, or run qawolf runner launch first.";
156396
+ var interactiveRunnerMessages = {
156397
+ actionFailed: (errorMessage2) => `The action reached the runner and did not take effect: ${errorMessage2}`,
156398
+ actionFlagsWithStdin: '"-" reads the whole action from stdin, so an action flag passed alongside it would be ignored. Pipe the action in on its own, or name the action type and use flags.',
156399
+ 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.",
156400
+ actionNotJson: `Stdin did not hold a JSON action. Pipe one object, for example '{"type":"click","button":"left","x":480,"y":260}'.`,
156401
+ actionPerformed: (type) => `Performed ${type}.`,
156402
+ alreadyRunning: (id) => `Runner ${id} was already running.`,
156403
+ defaultNotRemembered: (id) => `Runner ${id} could not be written to .qawolf as this directory's default, so later commands will not find it on their own. Pass --runner ${id}, or set QAWOLF_RUNNER_ID=${id}.`,
156404
+ defaultNotForgotten: (id) => `Runner ${id} was stopped, but it could not be removed from .qawolf as this directory's default, so later commands will still be sent to it. Pass --runner, or launch a new runner.`,
156405
+ fileNotCollected: (path) => `"${path}" is not one of the files that travel to a runner. It must be inside the current directory and end in .ts, .tsx, .js, .mjs, .cjs or .json.`,
156406
+ fileUnreadable: (path, reason) => `${path} is one of the files that travel to a runner, but it could not be read: ${reason}. Move it out of the current directory, or make it readable.`,
156407
+ filesUnreadable: (reason) => `One of the files that travel to a runner could not be read: ${reason}.`,
156408
+ filesTooLarge: (byteLength, maxByteLength, largest) => `The collected files carry ${String(byteLength)} bytes, over the ${String(maxByteLength)} a single run may ship. The largest are ${largest.map((file) => `${file.path} (${String(file.byteLength)} bytes)`).join(", ")}. Every .ts, .tsx, .js, .mjs, .cjs and .json file under the current directory travels, build output included, so run from a directory holding only the flow and what it imports.`,
156409
+ keptAlive: (id) => `Runner ${id} is alive, and its inactivity clock has been reset.`,
156410
+ launchFailed: (id, error) => `Launching runner ${id} failed. ${error}`,
156411
+ launchLost: (id, error) => `Launching runner ${id} failed. ${error}. The request may still have reached QA Wolf and started a runner, so relaunch the same id with qawolf runner launch --id ${id} to attach to it rather than start a second one, or stop it with qawolf runner stop --runner ${id}.`,
156412
+ launched: (id) => `Launched runner ${id}.`,
156413
+ launchedForCommand: (id) => `No runner was given, so launched ${id} for this command. Its browser is fresh: nothing has been run on it and nothing is signed in. It bills until it is stopped or idles out, so stop it with qawolf runner stop --runner ${id} when you are done.`,
156414
+ followEventsTimedOut: (stream, seconds) => `Stopped following ${stream} after ${formatSeconds(seconds * 1000)}: reading keeps the runner alive and billing, so a follow does not run unbounded. Pass --timeout to wait longer, or follow again to continue.`,
156415
+ followTimedOut: (runId, runnerId, seconds) => `Stopped following run ${runId} after ${formatSeconds(seconds * 1000)}. The run may still be going: read it with qawolf runner events run-status --run ${runId}, and stop the runner with qawolf runner stop --runner ${runnerId} when you are done. Pass --timeout to wait longer.`,
156416
+ missingPackageJson: "No package.json in the current directory. A run reads its npm dependencies from one, so it has to travel with the flow.",
156417
+ noRunnerIdForScreenshot: `${noRunnerId} A screenshot also needs a screen, which a runner gets from its first run: run a flow on it with qawolf runner run.`,
156418
+ noRunnerId,
156419
+ notRunning: (id) => `Runner ${id} was not running.`,
156420
+ runFailed: (errorMessage2) => errorMessage2 === undefined ? "The run failed and reported no reason." : `The run failed: ${errorMessage2}`,
156421
+ runPassed: "The run passed.",
156422
+ runSettledUnknown: (status) => `The run settled as "${status}", which this version of the CLI does not recognize. Upgrade to read it.`,
156423
+ runSubmitAnsweredUnknown: (outcome) => `The runner answered the submission with "${outcome}", which this version of the CLI does not recognize. Upgrade to read it.`,
156424
+ runSubmitted: (runId) => `Submitted run ${runId}.`,
156425
+ 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 node20WithPlaywright runner instead.",
156426
+ runnerHasNoScreenToEvaluate: "The runner could not evaluate the snippet. This covers a runner that is still starting or busy, and also one with no live page to evaluate against: a freshly launched runner has no page until a run opens one, so run a flow on it with qawolf runner run first. If it is not a runner that runs a browser at all, this will never clear. It is not proof the snippet did not run, so do not resubmit one that mutates the page without reading qawolf runner events console first.",
156427
+ runnerUnreachable: "The runner could not be reached. It may still be starting, or it may have terminated after inactivity. Retry, or launch it again.",
156428
+ 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.",
156429
+ 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.",
156430
+ 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.",
156431
+ screenshotUnwritable: (path, detail) => `The screen was captured but could not be written to "${path}": ${detail}. Give --out a path this process can write to.`,
156432
+ screenshotWritten: (path) => `Wrote the runner's screen to ${path}.`,
156433
+ skippedEntries: (stream, count) => `${pluralize(count, "entry", "entries")} of ${stream} were dropped before this read: the runner keeps a bounded history and this follow has been overtaken.`,
156434
+ snippetEmpty: (path) => `"${path}" holds no code to evaluate.`,
156435
+ snippetErrored: (errorMessage2) => errorMessage2 === undefined ? "The snippet threw and reported no message." : `The snippet threw: ${errorMessage2}`,
156436
+ snippetFileUnreadable: (path) => `Could not read "${path}". Name a readable file, or "-" to read the snippet from stdin.`,
156437
+ snippetRan: "The snippet ran. Its value is not returned: read anything it printed with qawolf runner events console.",
156438
+ snippetStopped: "The snippet was interrupted before it finished. Anything it printed first is in qawolf runner events console.",
156439
+ stdinEmptyAction: 'Nothing arrived on stdin. Pipe the action in, or name the action type instead of "-".',
156440
+ stdinEmptySnippet: 'Nothing arrived on stdin. Pipe the snippet in, or name a file instead of "-".',
156441
+ stopped: (id) => `Stopped runner ${id}.`,
156442
+ requestTooLarge: (byteLength, maxByteLength) => `The run request encodes to ${String(byteLength)} bytes, over the ${String(maxByteLength)} a runner accepts. Escaping inflates file content, so a set of files inside the content cap can still encode to more than this. Run from a directory holding only the flow and what it imports.`,
156443
+ unknownStream: (stream, known) => `"${stream}" is not a stream QA Wolf writes, so this read has nothing to return. QA Wolf writes ${known.join(", ")}.`,
156444
+ unsearchedHistory: (stream) => `This read of ${stream} stopped before searching all of the runner's history, so older entries may exist that it did not look at. Page forwards with --since to reach them.`,
156445
+ submitMayHaveStarted: "The runner did not answer, which does not mean the run did not start. Read qawolf runner events run-status before running the flow again, and use the newest run id there if one appeared.",
156446
+ targetMismatch: (runnerName, requiredRunnerName) => `This flow needs a ${requiredRunnerName} runner and ${runnerName} is running. Launch one with --name ${requiredRunnerName}.`
156447
+ };
156394
156448
  // src/core/messages/runner.ts
156395
156449
  var runnerMessages = {
156396
156450
  playwrightLoadFailed: (envDir, detail) => packageLoadFailed("Playwright", envDir, detail),
@@ -156543,7 +156597,8 @@ var exitCodes = {
156543
156597
  invalidArgs: 2,
156544
156598
  auth: 3,
156545
156599
  network: 4,
156546
- config: 5
156600
+ config: 5,
156601
+ timeout: 6
156547
156602
  };
156548
156603
  function exit(code, message, proc = process) {
156549
156604
  if (message !== undefined && message.length > 0) {
@@ -171279,7 +171334,12 @@ async function requestWithRetry(args) {
171279
171334
  const backoff = args.backoffMs[attempt];
171280
171335
  const retryable = result.error.kind === "network" || result.error.kind === "timeout";
171281
171336
  if (backoff === undefined || !retryable) {
171282
- return { ok: false, ...args.describe(result.error) };
171337
+ const mayHaveArrived = result.error.kind !== "http";
171338
+ return {
171339
+ ok: false,
171340
+ ...args.describe(result.error),
171341
+ ...mayHaveArrived ? { mayHaveArrived } : {}
171342
+ };
171283
171343
  }
171284
171344
  await sleep2(backoff);
171285
171345
  }
@@ -172427,7 +172487,6 @@ var browserActionSchema = exports_external.discriminatedUnion("type", [
172427
172487
  type: exports_external.literal("type")
172428
172488
  })
172429
172489
  ]);
172430
-
172431
172490
  // node_modules/@qawolf/api-contracts/dist/v1/runner/environment.js
172432
172491
  var reservedRunEnvironmentVariablePrefix = "QAWOLF_";
172433
172492
  function refusalForRunEnvironmentVariableName(name) {
@@ -172488,7 +172547,6 @@ var runFilePathSchema = exports_external.string().refine(isShippableRunFilePath,
172488
172547
  error: runFilePathRefusal
172489
172548
  });
172490
172549
  var runPackageJsonPath = "package.json";
172491
-
172492
172550
  // node_modules/@qawolf/api-contracts/dist/v1/runner/identity.js
172493
172551
  var runnerNameSchema = exports_external.enum([
172494
172552
  "node20Basic",
@@ -172502,7 +172560,6 @@ var makeRunnerSchema = () => exports_external.object({
172502
172560
  id: runnerIdSchema.describe("The runner's id, as supplied when launching it."),
172503
172561
  runnerName: runnerNameSchema.describe("The image requested when the runner was launched. QA Wolf may place it on faster hardware, reported in gpuAccelerated.")
172504
172562
  });
172505
-
172506
172563
  // node_modules/@qawolf/api-contracts/dist/v1/runner/journal.js
172507
172564
  var journalEntrySchema = exports_external.object({
172508
172565
  payload: exports_external.unknown(),
@@ -172537,7 +172594,6 @@ var readJournalResponseSchema = exports_external.object({
172537
172594
  nextSequence: exports_external.number().int().nonnegative(),
172538
172595
  oldestAvailableSequence: exports_external.number().int().nonnegative()
172539
172596
  });
172540
-
172541
172597
  // node_modules/@qawolf/api-contracts/dist/v1/runner/payloadSize.js
172542
172598
  var maxRunFilesByteLength = 4 * 1024 * 1024;
172543
172599
  var maxRunnerRequestEncodedByteLength = 9 * 1024 * 1024;
@@ -172560,7 +172616,6 @@ var runFilesSchema = exports_external.record(exports_external.string(), exports_
172560
172616
  }).refine((files) => runFilesByteLength(files) <= maxRunFilesByteLength, {
172561
172617
  error: `The files in one request may carry at most ${maxRunFilesByteLength} bytes in total.`
172562
172618
  });
172563
-
172564
172619
  // node_modules/@qawolf/api-contracts/dist/v1/runner/screen.js
172565
172620
  var runnerScreenNotReadySchema = exports_external.object({
172566
172621
  outcome: exports_external.literal("screen-not-ready")
@@ -172666,6 +172721,7 @@ var makeEnvironmentResourceSchema = (ids) => resource({
172666
172721
  })
172667
172722
  ]).optional().describe("The environment's flow code branch and its git reconciliation state, if it has one."),
172668
172723
  id: ids.environment,
172724
+ isDefault: exports_external.boolean().describe("Whether this is the workspace's default environment."),
172669
172725
  kind: exports_external.enum(["static", "preview"]).describe("Whether the environment is static or a preview."),
172670
172726
  name: exports_external.string(),
172671
172727
  runConcurrencyLimit: exports_external.string().describe('Maximum number of concurrent runs: a whole number, "unlimited", or a concurrency expression.'),
@@ -172681,6 +172737,7 @@ var makeFindEnvironmentsContract = (ids) => {
172681
172737
  ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 100 })
172682
172738
  });
172683
172739
  const output = exports_external.object({
172740
+ defaultEnvironmentId: ids.environment.describe("The id of the workspace's default environment."),
172684
172741
  environments: exports_external.array(makeEnvironmentResourceSchema(ids)).describe("The team's environments, newest first."),
172685
172742
  nextCursor: nextCursorSchema
172686
172743
  });
@@ -172754,7 +172811,7 @@ var makeSetEnvironmentVariableContract = (ids) => {
172754
172811
  name: exports_external.string().describe("The stored variable name after whitespace is replaced with underscores and letters are uppercased.")
172755
172812
  });
172756
172813
  return {
172757
- description: 'Create or replace an environment variable. If the user asks to create one for "my email" without naming it, use DEFAULT_ENVIRONMENT_EMAIL. The value is never returned.',
172814
+ description: 'Create or replace an environment variable. If the user asks to create one for "my email" without naming it, use DEFAULT_EMAIL. The value is never returned.',
172758
172815
  input,
172759
172816
  kind: "write",
172760
172817
  name: "environment.setVariable",
@@ -173056,8 +173113,50 @@ var makeGetRunContract = (ids) => {
173056
173113
  const input = exports_external.object({
173057
173114
  runId: ids.run
173058
173115
  });
173116
+ const artifactUrls = {
173117
+ logsUrl: exports_external.url().optional().describe("Signed URL for the attempt's execution logs."),
173118
+ traceUrl: exports_external.url().optional().describe("Signed URL for the attempt's Playwright trace (a trace.zip; open it with `npx playwright show-trace`)."),
173119
+ videoUrl: exports_external.url().optional().describe("Signed URL for the attempt's screen recording.")
173120
+ };
173121
+ const automatedKind = exports_external.literal("automated").describe("The attempt ran on a QA Wolf runner.");
173122
+ const automatedAttempt = exports_external.discriminatedUnion("status", [
173123
+ exports_external.object({
173124
+ ...artifactUrls,
173125
+ completedAt: exports_external.iso.datetime(),
173126
+ kind: automatedKind,
173127
+ startedAt: exports_external.iso.datetime(),
173128
+ status: exports_external.literal("passed")
173129
+ }),
173130
+ exports_external.object({
173131
+ ...artifactUrls,
173132
+ completedAt: exports_external.iso.datetime(),
173133
+ kind: automatedKind,
173134
+ startedAt: exports_external.iso.datetime().optional().describe("Absent when the attempt failed before it could start."),
173135
+ status: exports_external.literal("failed")
173136
+ }),
173137
+ exports_external.object({
173138
+ completedAt: exports_external.iso.datetime().optional(),
173139
+ kind: automatedKind,
173140
+ startedAt: exports_external.iso.datetime().optional(),
173141
+ status: exports_external.literal("canceled").describe("The attempt was terminated without reaching a verdict.")
173142
+ })
173143
+ ]);
173144
+ const manualAttempt = exports_external.object({
173145
+ completedAt: exports_external.iso.datetime(),
173146
+ kind: exports_external.literal("manual").describe("The attempt ran by hand in the Wolf Browser."),
173147
+ startedAt: exports_external.iso.datetime(),
173148
+ status: exports_external.literal("passed")
173149
+ });
173150
+ const attempt = exports_external.union([automatedAttempt, manualAttempt]);
173151
+ const attempts = exports_external.array(attempt).optional().describe("The flow's finished execution attempts, oldest first, including " + "manual Wolf Browser attempts. Present once at least one attempt has " + "finished, so a flow that passed after retries also lists its failed " + "attempts. Artifact URLs appear only on automated attempts that " + "reached a verdict, stay valid for at least a day (call run.get " + "again for fresh ones), and can return 404 when the attempt did not " + "produce that artifact.");
173152
+ const diagnosis = exports_external.object({
173153
+ issueId: ids.issue,
173154
+ type: exports_external.enum(["bug", "maintenance"])
173155
+ }).optional().describe("QA Wolf's investigation verdict for the failure: `bug` means the " + "application is broken, `maintenance` means the test needed an " + "update and the failure does not indicate an application problem. " + "Absent until the investigation reaches a verdict. Pass issueId to " + "issue.get for details.");
173059
173156
  const failedFlow = exports_external.object({
173157
+ attempts,
173060
173158
  failure: exports_external.object({
173159
+ diagnosis,
173061
173160
  error: exports_external.string()
173062
173161
  }),
173063
173162
  flowId: ids.flow,
@@ -173065,6 +173164,7 @@ var makeGetRunContract = (ids) => {
173065
173164
  status: exports_external.literal("failed")
173066
173165
  });
173067
173166
  const nonFailedFlow = exports_external.object({
173167
+ attempts,
173068
173168
  flowId: ids.flow,
173069
173169
  name: exports_external.string(),
173070
173170
  status: exports_external.enum(["queued", "running", "passed", "canceled"])
@@ -175510,6 +175610,8 @@ function createAgentRenderers() {
175510
175610
  writeStderrLine(humanMessage);
175511
175611
  },
175512
175612
  gap: () => writeStderrLine(""),
175613
+ stream: (_data, line) => writeStdoutRaw(`${line}
175614
+ `),
175513
175615
  write: (text) => writeStderrRaw(text),
175514
175616
  withProgress: async (steps, done) => {
175515
175617
  const results = [];
@@ -175545,6 +175647,8 @@ ${body}` : ""));
175545
175647
  },
175546
175648
  output: (_data, humanMessage) => clack.log.info(humanMessage),
175547
175649
  gap: () => process.stderr.write(`
175650
+ `),
175651
+ stream: (_data, line) => writeStdoutRaw(`${line}
175548
175652
  `),
175549
175653
  write: (text) => writeStdoutRaw(text),
175550
175654
  withProgress: async (steps, done) => {
@@ -175618,6 +175722,7 @@ function createJsonRenderers() {
175618
175722
  error: (title, body) => writeJsonDiagnostic({ type: "error", title, body }),
175619
175723
  output: (data, _humanMessage) => writeJsonLine(data),
175620
175724
  gap: () => {},
175725
+ stream: (data, _line) => writeJsonLine(data),
175621
175726
  write: () => {},
175622
175727
  withProgress: async (steps, done) => {
175623
175728
  const results = [];
@@ -175804,7 +175909,7 @@ function startUpdateCheck(deps) {
175804
175909
  // package.json
175805
175910
  var package_default = {
175806
175911
  name: "@qawolf/cli",
175807
- version: "1.8.1",
175912
+ version: "1.9.0",
175808
175913
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
175809
175914
  keywords: [
175810
175915
  "automation",
@@ -175864,7 +175969,7 @@ var package_default = {
175864
175969
  "@clack/prompts": "1.5.1",
175865
175970
  "@napi-rs/keyring": "1.3.0",
175866
175971
  "@oxc-node/core": "0.1.0",
175867
- "@qawolf/api-contracts": "0.24.0",
175972
+ "@qawolf/api-contracts": "0.25.0",
175868
175973
  "@qawolf/emails": "1.1.1",
175869
175974
  "@qawolf/flow-targets": "1.0.0",
175870
175975
  "@qawolf/flows": "0.1.4",
@@ -185988,16 +186093,29 @@ async function handlePublicApiCommand(ctx, spec, options) {
185988
186093
  return;
185989
186094
  }
185990
186095
 
186096
+ // src/domains/publicApi/skippedContracts.ts
186097
+ var handWrittenContractNames = new Set([
186098
+ "flow.list",
186099
+ "runner.evaluateSnippet",
186100
+ "runner.launch",
186101
+ "runner.performAction",
186102
+ "runner.readJournal",
186103
+ "runner.runFlow",
186104
+ "runner.stop",
186105
+ "runner.takeScreenshot"
186106
+ ]);
186107
+ var unexpressibleContractNames = new Set([
186108
+ "issue.update"
186109
+ ]);
186110
+ var skippedContractNames = new Set([
186111
+ ...handWrittenContractNames,
186112
+ ...unexpressibleContractNames
186113
+ ]);
186114
+
185991
186115
  // src/commands/publicApi/index.ts
185992
186116
  var groupDescriptions = {
185993
186117
  run: "Trigger and manage QA Wolf runs on the platform"
185994
186118
  };
185995
- var handWrittenContractNames = new Set(["flow.list"]);
185996
- var skippedContractNames = new Set([
185997
- ...handWrittenContractNames,
185998
- "issue.update",
185999
- "runner.performAction"
186000
- ]);
186001
186119
  var optionEnvironmentVariables = new Map([
186002
186120
  ["environmentId", "QAWOLF_ENVIRONMENT"],
186003
186121
  ["public.run.create.aiTaskId", "QAWOLF_AI_TASK_ID"]
@@ -186044,6 +186162,1016 @@ function registerPublicApiCommands(program2, signals, options = {}) {
186044
186162
  }
186045
186163
  }
186046
186164
 
186165
+ // src/core/interactiveRunner/runFiles.ts
186166
+ import { isAbsolute as isAbsolute5, relative as relative8, resolve as resolve11, sep as sep5 } from "node:path";
186167
+ function toCollectedPath(cwd, path9) {
186168
+ const absolute = isAbsolute5(path9) ? path9 : resolve11(cwd, path9);
186169
+ return relative8(cwd, absolute).split(sep5).join("/");
186170
+ }
186171
+ var namedLargestFileCount = 3;
186172
+ function largestFiles(files) {
186173
+ return Object.entries(files).map(([path9, content]) => ({
186174
+ byteLength: runFilesByteLength({ [path9]: content }),
186175
+ path: path9
186176
+ })).sort((a3, b4) => b4.byteLength - a3.byteLength).slice(0, namedLargestFileCount);
186177
+ }
186178
+ function checkPresenceAndSize(files, requiredPath) {
186179
+ if (!Object.hasOwn(files, requiredPath)) {
186180
+ return { path: requiredPath, type: "missing-file" };
186181
+ }
186182
+ const byteLength = runFilesByteLength(files);
186183
+ if (byteLength > maxRunFilesByteLength) {
186184
+ return {
186185
+ byteLength,
186186
+ largest: largestFiles(files),
186187
+ maxByteLength: maxRunFilesByteLength,
186188
+ type: "too-large"
186189
+ };
186190
+ }
186191
+ const encodedByteLength = Buffer.byteLength(JSON.stringify({ files, path: requiredPath }), "utf8");
186192
+ if (encodedByteLength > maxRunnerRequestEncodedByteLength) {
186193
+ return {
186194
+ byteLength: encodedByteLength,
186195
+ maxByteLength: maxRunnerRequestEncodedByteLength,
186196
+ type: "request-too-large"
186197
+ };
186198
+ }
186199
+ return { type: "ok" };
186200
+ }
186201
+ function checkRunFiles(files, entryPointPath) {
186202
+ const present = checkPresenceAndSize(files, entryPointPath);
186203
+ if (present.type !== "ok")
186204
+ return present;
186205
+ if (!Object.hasOwn(files, runPackageJsonPath)) {
186206
+ return { type: "missing-package-json" };
186207
+ }
186208
+ return { type: "ok" };
186209
+ }
186210
+ function checkSnippetFiles(files, contextFilePath) {
186211
+ return checkPresenceAndSize(files, contextFilePath);
186212
+ }
186213
+ function describeRunFilesCheck(check2) {
186214
+ switch (check2.type) {
186215
+ case "missing-file":
186216
+ return interactiveRunnerMessages.fileNotCollected(check2.path);
186217
+ case "missing-package-json":
186218
+ return interactiveRunnerMessages.missingPackageJson;
186219
+ case "too-large":
186220
+ return interactiveRunnerMessages.filesTooLarge(check2.byteLength, check2.maxByteLength, check2.largest);
186221
+ case "request-too-large":
186222
+ return interactiveRunnerMessages.requestTooLarge(check2.byteLength, check2.maxByteLength);
186223
+ }
186224
+ }
186225
+
186226
+ // src/domains/interactiveRunner/runnerIds.ts
186227
+ function parseRunnerId(id) {
186228
+ const parsed = runnerIdSchema.safeParse(id);
186229
+ if (!parsed.success)
186230
+ return { error: exports_external.prettifyError(parsed.error), ok: false };
186231
+ return { id: parsed.data, ok: true };
186232
+ }
186233
+ function parseRunnerName(runnerName) {
186234
+ const parsed = runnerNameSchema.safeParse(runnerName);
186235
+ if (!parsed.success)
186236
+ return { error: exports_external.prettifyError(parsed.error), ok: false };
186237
+ return { ok: true, runnerName: parsed.data };
186238
+ }
186239
+
186240
+ // src/domains/interactiveRunner/launch.ts
186241
+ async function launchRunner(ctx, options) {
186242
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.launch, {
186243
+ id: options.id,
186244
+ ...options.runnerName ? { runnerName: options.runnerName } : {}
186245
+ });
186246
+ if (!result.ok) {
186247
+ return {
186248
+ error: result.error,
186249
+ exitCode: exitCodes.network,
186250
+ mayHaveArrived: result.mayHaveArrived ?? false,
186251
+ ok: false
186252
+ };
186253
+ }
186254
+ return { ok: true, value: result.value };
186255
+ }
186256
+ async function launchAndRemember(ctx, options, deps) {
186257
+ const priorDefault = await deps.store.readDefaultRunnerId().catch(() => {
186258
+ return;
186259
+ });
186260
+ const remembered = await deps.store.writeDefaultRunnerId(options.id).then(() => true).catch(() => {
186261
+ ctx.ui.warn(interactiveRunnerMessages.defaultNotRemembered(options.id));
186262
+ return false;
186263
+ });
186264
+ const launched = await launchRunner(ctx, options);
186265
+ if (launched.ok)
186266
+ return launched;
186267
+ if (remembered && !launched.mayHaveArrived) {
186268
+ await (priorDefault === undefined ? deps.store.clearDefaultRunnerId() : deps.store.writeDefaultRunnerId(priorDefault)).catch(() => {
186269
+ return;
186270
+ });
186271
+ }
186272
+ return {
186273
+ error: launched.mayHaveArrived ? interactiveRunnerMessages.launchLost(options.id, launched.error) : interactiveRunnerMessages.launchFailed(options.id, launched.error),
186274
+ exitCode: launched.exitCode,
186275
+ mayHaveArrived: launched.mayHaveArrived,
186276
+ ok: false
186277
+ };
186278
+ }
186279
+ async function handleRunnerLaunch(ctx, options, deps) {
186280
+ const chosenId = options.id ?? deps.makeRunnerId();
186281
+ const id = parseRunnerId(chosenId);
186282
+ if (!id.ok)
186283
+ return { error: id.error, exitCode: exitCodes.invalidArgs };
186284
+ const runnerName = options.name === undefined ? undefined : parseRunnerName(options.name);
186285
+ if (runnerName !== undefined && !runnerName.ok) {
186286
+ return { error: runnerName.error, exitCode: exitCodes.invalidArgs };
186287
+ }
186288
+ const launched = await launchAndRemember(ctx, { id: id.id, runnerName: runnerName?.runnerName }, deps);
186289
+ if (!launched.ok) {
186290
+ return { error: launched.error, exitCode: launched.exitCode };
186291
+ }
186292
+ ctx.ui.output(launched.value, launched.value.outcome === "launched" ? interactiveRunnerMessages.launched(launched.value.id) : interactiveRunnerMessages.alreadyRunning(launched.value.id));
186293
+ return;
186294
+ }
186295
+
186296
+ // src/domains/interactiveRunner/resolveRunner.ts
186297
+ var runnerIdEnvironmentVariable = "QAWOLF_RUNNER_ID";
186298
+ async function chooseRunnerId(runner, deps) {
186299
+ if (runner !== undefined)
186300
+ return runner;
186301
+ const fromEnvironment = deps.env[runnerIdEnvironmentVariable]?.trim();
186302
+ if (fromEnvironment)
186303
+ return fromEnvironment;
186304
+ return deps.store.readDefaultRunnerId();
186305
+ }
186306
+ async function resolveRunner(ctx, options, deps) {
186307
+ const chosen = await chooseRunnerId(options.runner, deps);
186308
+ if (chosen !== undefined) {
186309
+ const parsed = parseRunnerId(chosen);
186310
+ return parsed.ok ? { runnerId: parsed.id, type: "resolved" } : {
186311
+ error: parsed.error,
186312
+ exitCode: exitCodes.invalidArgs,
186313
+ type: "failed"
186314
+ };
186315
+ }
186316
+ if (!options.autoLaunch) {
186317
+ return {
186318
+ error: options.noRunnerIdMessage ?? interactiveRunnerMessages.noRunnerId,
186319
+ exitCode: exitCodes.invalidArgs,
186320
+ type: "failed"
186321
+ };
186322
+ }
186323
+ const launched = await launchAndRemember(ctx, { id: deps.makeRunnerId(), runnerName: undefined }, deps);
186324
+ if (!launched.ok) {
186325
+ return {
186326
+ error: launched.error,
186327
+ exitCode: launched.exitCode,
186328
+ type: "failed"
186329
+ };
186330
+ }
186331
+ return { runnerId: launched.value.id, type: "launched" };
186332
+ }
186333
+ function announceRunner(ctx, resolved) {
186334
+ if (resolved.type === "launched") {
186335
+ ctx.ui.info(interactiveRunnerMessages.launchedForCommand(resolved.runnerId));
186336
+ }
186337
+ }
186338
+
186339
+ // src/domains/interactiveRunner/evaluateSnippet.ts
186340
+ var stdinArgument = "-";
186341
+ async function resolveScope(contextFile, deps) {
186342
+ if (contextFile === undefined) {
186343
+ return { filePath: undefined, files: undefined, ok: true };
186344
+ }
186345
+ const filePath = toCollectedPath(deps.cwd, contextFile);
186346
+ const files = await deps.collectRunFiles();
186347
+ const check2 = checkSnippetFiles(files, filePath);
186348
+ if (check2.type !== "ok") {
186349
+ return { error: describeRunFilesCheck(check2), ok: false };
186350
+ }
186351
+ return { filePath, files, ok: true };
186352
+ }
186353
+ async function readCode(source, deps) {
186354
+ const code = source === stdinArgument ? await deps.readStdin() : await deps.readFile(source).catch(() => {
186355
+ return;
186356
+ });
186357
+ if (code === undefined) {
186358
+ return {
186359
+ error: interactiveRunnerMessages.snippetFileUnreadable(source),
186360
+ ok: false
186361
+ };
186362
+ }
186363
+ if (code.trim() === "") {
186364
+ return source === stdinArgument ? { error: interactiveRunnerMessages.stdinEmptySnippet, ok: false } : { error: interactiveRunnerMessages.snippetEmpty(source), ok: false };
186365
+ }
186366
+ return { code, ok: true };
186367
+ }
186368
+ async function handleRunnerExec(ctx, options, deps) {
186369
+ const code = await readCode(options.source, deps);
186370
+ if (!code.ok)
186371
+ return { error: code.error, exitCode: exitCodes.invalidArgs };
186372
+ const scope = await resolveScope(options.contextFile, deps);
186373
+ if (!scope.ok)
186374
+ return { error: scope.error, exitCode: exitCodes.invalidArgs };
186375
+ const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
186376
+ if (resolved.type === "failed") {
186377
+ return { error: resolved.error, exitCode: resolved.exitCode };
186378
+ }
186379
+ announceRunner(ctx, resolved);
186380
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.evaluateSnippet, {
186381
+ code: code.code,
186382
+ id: resolved.runnerId,
186383
+ ...scope.filePath === undefined ? {} : { filePath: scope.filePath },
186384
+ ...scope.files === undefined ? {} : { files: scope.files }
186385
+ });
186386
+ if (!result.ok) {
186387
+ return { ...failureFields(result), exitCode: exitCodes.network };
186388
+ }
186389
+ if (result.value.outcome === "runner-unreachable") {
186390
+ return {
186391
+ error: interactiveRunnerMessages.runnerHasNoScreenToEvaluate,
186392
+ exitCode: exitCodes.network
186393
+ };
186394
+ }
186395
+ switch (result.value.result) {
186396
+ case "success":
186397
+ ctx.ui.output({ outcome: "evaluated", result: "success" }, interactiveRunnerMessages.snippetRan);
186398
+ return;
186399
+ case "stopped":
186400
+ return {
186401
+ error: interactiveRunnerMessages.snippetStopped,
186402
+ exitCode: exitCodes.testFailure
186403
+ };
186404
+ case "error":
186405
+ return {
186406
+ error: interactiveRunnerMessages.snippetErrored(result.value.errorMessage),
186407
+ exitCode: exitCodes.testFailure
186408
+ };
186409
+ }
186410
+ }
186411
+
186412
+ // src/core/interactiveRunner/browserAction.ts
186413
+ function parseJsonPath(path9) {
186414
+ try {
186415
+ return { ok: true, value: JSON.parse(path9) };
186416
+ } catch {
186417
+ return {
186418
+ error: `--path must be a JSON array of points, for example '[{"x":10,"y":20},{"x":80,"y":90}]'.`,
186419
+ ok: false
186420
+ };
186421
+ }
186422
+ }
186423
+ function toNumber(value) {
186424
+ return value.trim() === "" ? Number.NaN : Number(value);
186425
+ }
186426
+ function buildBrowserAction(type, flags) {
186427
+ const parsedPath = flags.path === undefined ? undefined : parseJsonPath(flags.path);
186428
+ if (parsedPath !== undefined && !parsedPath.ok)
186429
+ return parsedPath;
186430
+ const candidate = {
186431
+ type,
186432
+ ...flags.button === undefined ? {} : { button: flags.button },
186433
+ ...flags.keys === undefined ? {} : { keys: flags.keys },
186434
+ ...parsedPath === undefined ? {} : { path: parsedPath.value },
186435
+ ...flags.scrollX === undefined ? {} : { scroll_x: toNumber(flags.scrollX) },
186436
+ ...flags.scrollY === undefined ? {} : { scroll_y: toNumber(flags.scrollY) },
186437
+ ...flags.text === undefined ? {} : { text: flags.text },
186438
+ ...flags.url === undefined ? {} : { url: flags.url },
186439
+ ...flags.x === undefined ? {} : { x: toNumber(flags.x) },
186440
+ ...flags.y === undefined ? {} : { y: toNumber(flags.y) }
186441
+ };
186442
+ return parseBrowserAction(candidate);
186443
+ }
186444
+ function parseBrowserAction(candidate) {
186445
+ const parsed = browserActionSchema.safeParse(candidate);
186446
+ if (!parsed.success) {
186447
+ return { error: exports_external.prettifyError(parsed.error), ok: false };
186448
+ }
186449
+ return { action: parsed.data, ok: true };
186450
+ }
186451
+
186452
+ // src/domains/interactiveRunner/performAction.ts
186453
+ var stdinArgument2 = "-";
186454
+ async function readAction(type, flags, deps) {
186455
+ if (type !== stdinArgument2)
186456
+ return buildBrowserAction(type, flags);
186457
+ if (Object.values(flags).some((value) => value !== undefined)) {
186458
+ return { error: interactiveRunnerMessages.actionFlagsWithStdin, ok: false };
186459
+ }
186460
+ const piped = (await deps.readStdin()).trim();
186461
+ if (piped === "") {
186462
+ return { error: interactiveRunnerMessages.stdinEmptyAction, ok: false };
186463
+ }
186464
+ try {
186465
+ return parseBrowserAction(JSON.parse(piped));
186466
+ } catch {
186467
+ return { error: interactiveRunnerMessages.actionNotJson, ok: false };
186468
+ }
186469
+ }
186470
+ async function handleRunnerAct(ctx, options, deps) {
186471
+ const built = await readAction(options.type, options.flags, deps);
186472
+ if (!built.ok)
186473
+ return { error: built.error, exitCode: exitCodes.invalidArgs };
186474
+ const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
186475
+ if (resolved.type === "failed") {
186476
+ return { error: resolved.error, exitCode: resolved.exitCode };
186477
+ }
186478
+ announceRunner(ctx, resolved);
186479
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.performAction, { action: built.action, id: resolved.runnerId });
186480
+ if (!result.ok) {
186481
+ const fields = failureFields(result);
186482
+ return {
186483
+ ...fields,
186484
+ ...result.mayHaveArrived ? {
186485
+ error: `${fields.error} ${interactiveRunnerMessages.actionMayHaveHappened}`
186486
+ } : {},
186487
+ exitCode: exitCodes.network
186488
+ };
186489
+ }
186490
+ switch (result.value.outcome) {
186491
+ case "performed":
186492
+ ctx.ui.output({ action: built.action, outcome: "performed" }, interactiveRunnerMessages.actionPerformed(built.action.type));
186493
+ return;
186494
+ case "action-failed":
186495
+ return {
186496
+ error: interactiveRunnerMessages.actionFailed(result.value.errorMessage),
186497
+ exitCode: exitCodes.testFailure
186498
+ };
186499
+ case "screen-needs-a-run":
186500
+ return {
186501
+ error: interactiveRunnerMessages.screenNeedsARun,
186502
+ exitCode: exitCodes.invalidArgs
186503
+ };
186504
+ case "screen-not-ready":
186505
+ return {
186506
+ error: interactiveRunnerMessages.screenNotReady,
186507
+ exitCode: exitCodes.network
186508
+ };
186509
+ case "runner-has-no-screen":
186510
+ return {
186511
+ error: interactiveRunnerMessages.runnerHasNoScreen,
186512
+ exitCode: exitCodes.invalidArgs
186513
+ };
186514
+ case "runner-unreachable":
186515
+ return {
186516
+ error: interactiveRunnerMessages.actionMayHaveHappened,
186517
+ exitCode: exitCodes.network
186518
+ };
186519
+ }
186520
+ }
186521
+
186522
+ // src/domains/interactiveRunner/takeScreenshot.ts
186523
+ async function handleRunnerScreenshot(ctx, options, deps) {
186524
+ const resolved = await resolveRunner(ctx, {
186525
+ autoLaunch: false,
186526
+ noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForScreenshot,
186527
+ runner: options.runner
186528
+ }, deps);
186529
+ if (resolved.type === "failed") {
186530
+ return { error: resolved.error, exitCode: resolved.exitCode };
186531
+ }
186532
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.takeScreenshot, { id: resolved.runnerId });
186533
+ if (!result.ok) {
186534
+ return { ...failureFields(result), exitCode: exitCodes.network };
186535
+ }
186536
+ switch (result.value.outcome) {
186537
+ case "captured": {
186538
+ const written = await deps.writeScreenshot({
186539
+ imageJpegBase64: result.value.imageJpegBase64,
186540
+ path: options.out
186541
+ });
186542
+ if (!written.ok) {
186543
+ return written.reason === "not-a-jpeg" ? {
186544
+ error: interactiveRunnerMessages.screenshotNotAnImage,
186545
+ exitCode: exitCodes.network
186546
+ } : {
186547
+ error: interactiveRunnerMessages.screenshotUnwritable(options.out, written.detail),
186548
+ exitCode: exitCodes.invalidArgs
186549
+ };
186550
+ }
186551
+ ctx.ui.output({ outcome: "captured", path: options.out }, interactiveRunnerMessages.screenshotWritten(options.out));
186552
+ return;
186553
+ }
186554
+ case "screen-needs-a-run":
186555
+ return {
186556
+ error: interactiveRunnerMessages.screenNeedsARun,
186557
+ exitCode: exitCodes.invalidArgs
186558
+ };
186559
+ case "screen-not-ready":
186560
+ return {
186561
+ error: interactiveRunnerMessages.screenNotReady,
186562
+ exitCode: exitCodes.network
186563
+ };
186564
+ case "runner-has-no-screen":
186565
+ return {
186566
+ error: interactiveRunnerMessages.runnerHasNoScreen,
186567
+ exitCode: exitCodes.invalidArgs
186568
+ };
186569
+ case "runner-unreachable":
186570
+ return {
186571
+ error: interactiveRunnerMessages.runnerUnreachable,
186572
+ exitCode: exitCodes.network
186573
+ };
186574
+ }
186575
+ }
186576
+
186577
+ // src/shell/interactiveRunner/collectRunFiles.ts
186578
+ import { join as join42 } from "node:path";
186579
+ async function collectRunFiles(options) {
186580
+ const paths = await glob(shippableRunFileExtensions.map((extension) => `**/*${extension}`), {
186581
+ cwd: options.cwd,
186582
+ followSymbolicLinks: false,
186583
+ ignore: ["**/node_modules/**"]
186584
+ });
186585
+ const shippable = paths.filter(isShippableRunFilePath).sort();
186586
+ const files = {};
186587
+ const read = batchMap(shippable, async (path9) => ({
186588
+ content: await options.fs.readFile(join42(options.cwd, path9)),
186589
+ path: path9
186590
+ }), flowBatchSize);
186591
+ for await (const file2 of read)
186592
+ files[file2.path] = file2.content;
186593
+ return files;
186594
+ }
186595
+
186596
+ // src/shell/interactiveRunner/makeRunnerId.ts
186597
+ import { randomUUID } from "node:crypto";
186598
+ function makeRunnerId() {
186599
+ return `cli-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
186600
+ }
186601
+
186602
+ // src/shell/interactiveRunner/runnerStore.ts
186603
+ import { join as join43 } from "node:path";
186604
+ var storeFileName = "runner.json";
186605
+ var storeSchema = exports_external.object({ defaultRunnerId: exports_external.string().optional() });
186606
+ function parseJson(text3) {
186607
+ try {
186608
+ return JSON.parse(text3);
186609
+ } catch {
186610
+ return;
186611
+ }
186612
+ }
186613
+ function makeRunnerStore(options) {
186614
+ const directory = join43(options.cwd, qawolfDir);
186615
+ const path9 = join43(directory, storeFileName);
186616
+ let pendingWrites = 0;
186617
+ const nextPendingPath = () => `${path9}.${process.pid}.${++pendingWrites}.tmp`;
186618
+ return {
186619
+ async clearDefaultRunnerId() {
186620
+ await options.fs.rm(path9, { force: true });
186621
+ },
186622
+ async readDefaultRunnerId() {
186623
+ const contents2 = await options.fs.readFile(path9).catch(() => {
186624
+ return;
186625
+ });
186626
+ if (contents2 === undefined)
186627
+ return;
186628
+ const parsed = storeSchema.safeParse(parseJson(contents2));
186629
+ return parsed.success ? parsed.data.defaultRunnerId : undefined;
186630
+ },
186631
+ async writeDefaultRunnerId(runnerId) {
186632
+ const pendingPath = nextPendingPath();
186633
+ await options.fs.mkdir(directory, { recursive: true });
186634
+ await options.fs.writeFile(pendingPath, `${JSON.stringify({ defaultRunnerId: runnerId }, undefined, 2)}
186635
+ `);
186636
+ await options.fs.rename(pendingPath, path9);
186637
+ }
186638
+ };
186639
+ }
186640
+
186641
+ // src/shell/interactiveRunner/writeScreenshot.ts
186642
+ import { dirname as dirname14 } from "node:path";
186643
+ var jpegStartOfImage = [255, 216, 255];
186644
+ async function writeScreenshot(options) {
186645
+ const bytes = Buffer.from(options.imageJpegBase64, "base64");
186646
+ if (jpegStartOfImage.some((byte, index2) => bytes[index2] !== byte)) {
186647
+ return { ok: false, reason: "not-a-jpeg" };
186648
+ }
186649
+ try {
186650
+ await options.fs.mkdir(dirname14(options.path), { recursive: true });
186651
+ await options.fs.writeFile(options.path, bytes);
186652
+ } catch (error51) {
186653
+ return { detail: errorMessage(error51), ok: false, reason: "unwritable" };
186654
+ }
186655
+ return { ok: true };
186656
+ }
186657
+
186658
+ // src/shell/stdin.ts
186659
+ async function readStdin2() {
186660
+ if (process.stdin.isTTY === true)
186661
+ return "";
186662
+ const chunks = [];
186663
+ for await (const chunk of process.stdin)
186664
+ chunks.push(chunk);
186665
+ return Buffer.concat(chunks).toString("utf8");
186666
+ }
186667
+
186668
+ // src/domains/interactiveRunner/deps.ts
186669
+ function makeInteractiveRunnerDeps(options) {
186670
+ return {
186671
+ collectRunFiles: () => collectRunFiles({ cwd: options.cwd, fs: options.fs }),
186672
+ cwd: options.cwd,
186673
+ env: options.env,
186674
+ makeRunnerId,
186675
+ readFile: (path9) => options.fs.readFile(path9),
186676
+ readStdin: readStdin2,
186677
+ sleep,
186678
+ store: makeRunnerStore({ cwd: options.cwd, fs: options.fs }),
186679
+ writeScreenshot: (screenshot) => writeScreenshot({ ...screenshot, fs: options.fs })
186680
+ };
186681
+ }
186682
+
186683
+ // src/commands/runner/context.ts
186684
+ var runnerFlagDescription = "Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner";
186685
+ function runnerDeps(ctx) {
186686
+ return makeInteractiveRunnerDeps({
186687
+ cwd: process.cwd(),
186688
+ env: process.env,
186689
+ fs: ctx.fs
186690
+ });
186691
+ }
186692
+
186693
+ // src/commands/runner/interact.register.ts
186694
+ var defaultScreenshotPath = "screenshot.jpg";
186695
+ var screenshotExamples = `
186696
+ Examples:
186697
+ $ qawolf runner screenshot
186698
+ $ qawolf runner screenshot --out screens/step-3.jpg`;
186699
+ var actExamples = `
186700
+ Examples:
186701
+ $ qawolf runner act click --button left --x 480 --y 260
186702
+ $ qawolf runner act type --text "hello@example.com"
186703
+ $ qawolf runner act keypress --keys Control a
186704
+ $ qawolf runner act navigate --url https://example.com
186705
+ $ qawolf runner act drag --path '[{"x":10,"y":20},{"x":80,"y":90}]'
186706
+ $ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act -`;
186707
+ var execExamples = `
186708
+ Examples:
186709
+ $ qawolf runner exec snippet.ts
186710
+ $ echo 'console.log(await page.title())' | qawolf runner exec -
186711
+ $ qawolf runner exec snippet.ts --file flows/checkout.flow.ts`;
186712
+ function registerRunnerInteractCommands(runner, signals) {
186713
+ 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));
186714
+ declareCommandKind(runner.command("act <action>"), "write").description("Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin").option("--button <button>", "click: left, right, wheel, back or forward").option("--keys <keys...>", "keypress: modifiers and the key, e.g. Control a").option("--path <json>", "drag: JSON array of points to drag through").option("--runner <id>", runnerFlagDescription).option("--scroll-x <delta>", "scroll: horizontal wheel delta").option("--scroll-y <delta>", "scroll: vertical wheel delta").option("--text <text>", "type: the text to type").option("--url <url>", "navigate: the http or https URL to go to").option("--x <pixels>", "pointer x, in screenshot pixels").option("--y <pixels>", "pointer y, in screenshot pixels").addHelpText("after", actExamples).action((action2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
186715
+ flags: {
186716
+ button: opts.button,
186717
+ keys: opts.keys,
186718
+ path: opts.path,
186719
+ scrollX: opts.scrollX,
186720
+ scrollY: opts.scrollY,
186721
+ text: opts.text,
186722
+ url: opts.url,
186723
+ x: opts.x,
186724
+ y: opts.y
186725
+ },
186726
+ runner: opts.runner,
186727
+ type: action2
186728
+ }, runnerDeps(ctx)))(opts, command));
186729
+ 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((file2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerExec(ctx, { contextFile: opts.file, runner: opts.runner, source: file2 }, runnerDeps(ctx)))(opts, command));
186730
+ }
186731
+
186732
+ // src/domains/interactiveRunner/readJournal.ts
186733
+ function journalReadFailure(failed) {
186734
+ const { type: _type, ...failure } = failed;
186735
+ return failure;
186736
+ }
186737
+ var unreachableFailure = {
186738
+ error: interactiveRunnerMessages.runnerUnreachable,
186739
+ exitCode: exitCodes.network
186740
+ };
186741
+ async function readJournal(ctx, runnerId, request) {
186742
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.readJournal, {
186743
+ id: runnerId,
186744
+ stream: request.stream,
186745
+ ...request.runId === undefined ? {} : { runId: request.runId },
186746
+ ...request.sinceSequence === undefined ? {} : { sinceSequence: request.sinceSequence },
186747
+ ...request.tail === undefined ? {} : { tail: request.tail }
186748
+ });
186749
+ if (!result.ok) {
186750
+ return {
186751
+ ...failureFields(result),
186752
+ exitCode: exitCodes.network,
186753
+ type: "failed"
186754
+ };
186755
+ }
186756
+ if (result.value.outcome === "runner-unreachable") {
186757
+ return { type: "unreachable" };
186758
+ }
186759
+ return { type: "read", value: result.value };
186760
+ }
186761
+
186762
+ // src/domains/interactiveRunner/keepalive.ts
186763
+ async function handleRunnerKeepalive(ctx, options, deps) {
186764
+ const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
186765
+ if (resolved.type === "failed") {
186766
+ return { error: resolved.error, exitCode: resolved.exitCode };
186767
+ }
186768
+ const window2 = await readJournal(ctx, resolved.runnerId, {
186769
+ stream: "run-status",
186770
+ tail: 1
186771
+ });
186772
+ if (window2.type === "unreachable")
186773
+ return { ...unreachableFailure };
186774
+ if (window2.type === "failed")
186775
+ return window2;
186776
+ ctx.ui.output({ id: resolved.runnerId, outcome: "alive" }, interactiveRunnerMessages.keptAlive(resolved.runnerId));
186777
+ return;
186778
+ }
186779
+
186780
+ // src/domains/interactiveRunner/stop.ts
186781
+ async function handleRunnerStop(ctx, options, deps) {
186782
+ const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
186783
+ if (resolved.type === "failed") {
186784
+ return { error: resolved.error, exitCode: resolved.exitCode };
186785
+ }
186786
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.stop, {
186787
+ id: resolved.runnerId
186788
+ });
186789
+ if (!result.ok) {
186790
+ return { ...failureFields(result), exitCode: exitCodes.network };
186791
+ }
186792
+ await deps.store.readDefaultRunnerId().then(async (storedDefault) => {
186793
+ if (storedDefault === resolved.runnerId) {
186794
+ await deps.store.clearDefaultRunnerId();
186795
+ }
186796
+ }).catch(() => {
186797
+ ctx.ui.warn(interactiveRunnerMessages.defaultNotForgotten(resolved.runnerId));
186798
+ });
186799
+ ctx.ui.output(result.value, result.value.outcome === "stopped" ? interactiveRunnerMessages.stopped(result.value.id) : interactiveRunnerMessages.notRunning(result.value.id));
186800
+ return;
186801
+ }
186802
+
186803
+ // src/commands/runner/lifecycle.register.ts
186804
+ var launchExamples = `
186805
+ Examples:
186806
+ $ qawolf runner launch
186807
+ $ qawolf runner launch --name node20WithAndroid
186808
+ $ qawolf runner launch --id ci`;
186809
+ var keepaliveExamples = `
186810
+ Examples:
186811
+ $ qawolf runner keepalive
186812
+ $ qawolf runner keepalive --runner ci`;
186813
+ function registerRunnerLifecycleCommands(runner, signals) {
186814
+ declareCommandKind(runner.command("launch"), "write").description("Launch an interactive runner and make it this directory's default").option("--id <id>", "Id to launch under. Relaunching an id attaches to that runner").option("--name <image>", "Runner image to run, e.g. node20WithPlaywright").addHelpText("after", launchExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerLaunch(ctx, { id: opts.id, name: opts.name }, runnerDeps(ctx)))(opts, command));
186815
+ declareCommandKind(runner.command("stop"), "write").description("Stop an interactive runner").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerStop(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
186816
+ declareCommandKind(runner.command("keepalive"), "read").description("Reset a runner's inactivity clock, for a caller that pauses between actions").option("--runner <id>", runnerFlagDescription).addHelpText("after", keepaliveExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerKeepalive(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
186817
+ }
186818
+
186819
+ // src/core/interactiveRunner/followTimeout.ts
186820
+ var defaultFollowTimeoutSeconds = 3600;
186821
+ var followTimeoutSchema = exports_external.coerce.number().int().positive();
186822
+ function parseFollowTimeout(seconds) {
186823
+ if (seconds === undefined) {
186824
+ return { ok: true, seconds: defaultFollowTimeoutSeconds };
186825
+ }
186826
+ const parsed = followTimeoutSchema.safeParse(seconds);
186827
+ if (!parsed.success)
186828
+ return { error: exports_external.prettifyError(parsed.error), ok: false };
186829
+ return { ok: true, seconds: parsed.data };
186830
+ }
186831
+
186832
+ // src/core/interactiveRunner/journal.ts
186833
+ function formatJournalLine(entry, options) {
186834
+ const data2 = options.envelope ? entry : entry.payload;
186835
+ return { data: data2, line: JSON.stringify(data2) };
186836
+ }
186837
+ function countSkippedEntries(cursor3, oldestAvailableSequence) {
186838
+ if (oldestAvailableSequence <= cursor3 + 1)
186839
+ return 0;
186840
+ return oldestAvailableSequence - cursor3 - 1;
186841
+ }
186842
+ var runStatusPayloadSchema = exports_external.object({
186843
+ errorMessage: exports_external.string().optional(),
186844
+ runId: exports_external.string(),
186845
+ status: exports_external.string()
186846
+ });
186847
+ function readRunSettlement(payload) {
186848
+ const parsed = runStatusPayloadSchema.safeParse(payload);
186849
+ if (!parsed.success)
186850
+ return { type: "unreadable" };
186851
+ if (parsed.data.status === "in-progress")
186852
+ return { type: "in-progress" };
186853
+ return {
186854
+ errorMessage: parsed.data.errorMessage,
186855
+ status: parsed.data.status,
186856
+ type: "settled"
186857
+ };
186858
+ }
186859
+ var runLogPayloadSchema = exports_external.object({ message: exports_external.string() });
186860
+ function formatRunLogLine(payload) {
186861
+ const parsed = runLogPayloadSchema.safeParse(payload);
186862
+ return parsed.success ? parsed.data.message : JSON.stringify(payload);
186863
+ }
186864
+
186865
+ // src/domains/interactiveRunner/eventsOptions.ts
186866
+ var countSchema = exports_external.coerce.number().int().positive();
186867
+ var sequenceSchema = exports_external.coerce.number().int().nonnegative();
186868
+ function parseEventsOptions(options) {
186869
+ const parsed = exports_external.object({
186870
+ run: readJournalRequestSchema.shape.runId,
186871
+ since: sequenceSchema.optional(),
186872
+ stream: journalStreamSchema,
186873
+ tail: countSchema.optional()
186874
+ }).safeParse({
186875
+ stream: options.stream,
186876
+ ...options.run === undefined ? {} : { run: options.run },
186877
+ ...options.since === undefined ? {} : { since: options.since },
186878
+ ...options.tail === undefined ? {} : { tail: options.tail }
186879
+ });
186880
+ if (!parsed.success)
186881
+ return { error: exports_external.prettifyError(parsed.error), ok: false };
186882
+ return {
186883
+ ok: true,
186884
+ value: {
186885
+ runId: parsed.data.run,
186886
+ sinceSequence: parsed.data.since,
186887
+ stream: parsed.data.stream,
186888
+ tail: parsed.data.tail
186889
+ }
186890
+ };
186891
+ }
186892
+
186893
+ // src/domains/interactiveRunner/journalCursor.ts
186894
+ function createJournalCursor(ctx, runnerId, request) {
186895
+ let sinceSequence = request.sinceSequence;
186896
+ let tail = request.tail;
186897
+ let warnedUnsearchedHistory = false;
186898
+ return async function read() {
186899
+ const window2 = await readJournal(ctx, runnerId, {
186900
+ ...request,
186901
+ sinceSequence,
186902
+ tail
186903
+ });
186904
+ if (window2.type !== "read")
186905
+ return window2;
186906
+ if (sinceSequence !== undefined) {
186907
+ const skipped = countSkippedEntries(sinceSequence, window2.value.oldestAvailableSequence);
186908
+ if (skipped > 0) {
186909
+ ctx.ui.warn(interactiveRunnerMessages.skippedEntries(request.stream, skipped));
186910
+ }
186911
+ }
186912
+ if (window2.value.hasUnsearchedHistory && !warnedUnsearchedHistory) {
186913
+ warnedUnsearchedHistory = true;
186914
+ ctx.ui.warn(interactiveRunnerMessages.unsearchedHistory(request.stream));
186915
+ }
186916
+ tail = undefined;
186917
+ sinceSequence = Math.max(sinceSequence ?? 0, window2.value.nextSequence);
186918
+ return { entries: window2.value.entries, type: "entries" };
186919
+ };
186920
+ }
186921
+ var unreachableGraceMs = 60000;
186922
+ function createUnreachableBudget(pollIntervalMs2) {
186923
+ const limit = Math.ceil(unreachableGraceMs / pollIntervalMs2);
186924
+ let consecutive = 0;
186925
+ return {
186926
+ exhausted: () => ++consecutive >= limit,
186927
+ reset: () => {
186928
+ consecutive = 0;
186929
+ }
186930
+ };
186931
+ }
186932
+
186933
+ // src/domains/interactiveRunner/events.ts
186934
+ var pollIntervalMs2 = 1000;
186935
+ async function handleRunnerEvents(ctx, options, deps) {
186936
+ const parsed = parseEventsOptions(options);
186937
+ if (!parsed.ok)
186938
+ return { error: parsed.error, exitCode: exitCodes.invalidArgs };
186939
+ const timeout = parseFollowTimeout(options.timeout);
186940
+ if (!timeout.ok) {
186941
+ return { error: timeout.error, exitCode: exitCodes.invalidArgs };
186942
+ }
186943
+ const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
186944
+ if (resolved.type === "failed") {
186945
+ return { error: resolved.error, exitCode: resolved.exitCode };
186946
+ }
186947
+ const known = knownJournalStreams;
186948
+ if (!known.includes(parsed.value.stream)) {
186949
+ ctx.ui.warn(interactiveRunnerMessages.unknownStream(parsed.value.stream, known));
186950
+ }
186951
+ const read = createJournalCursor(ctx, resolved.runnerId, parsed.value);
186952
+ const unreachable = createUnreachableBudget(pollIntervalMs2);
186953
+ const maxPolls = Math.max(1, Math.ceil(timeout.seconds * 1000 / pollIntervalMs2));
186954
+ for (let poll = 1;; poll++) {
186955
+ const window2 = await read();
186956
+ if (window2.type === "failed")
186957
+ return journalReadFailure(window2);
186958
+ if (window2.type === "unreachable") {
186959
+ if (!options.follow || unreachable.exhausted()) {
186960
+ return { ...unreachableFailure };
186961
+ }
186962
+ } else {
186963
+ unreachable.reset();
186964
+ for (const entry of window2.entries) {
186965
+ const { data: data2, line } = formatJournalLine(entry, {
186966
+ envelope: options.envelope
186967
+ });
186968
+ ctx.ui.stream(data2, line);
186969
+ }
186970
+ if (!options.follow)
186971
+ return;
186972
+ }
186973
+ if (poll >= maxPolls) {
186974
+ return {
186975
+ error: interactiveRunnerMessages.followEventsTimedOut(parsed.value.stream, timeout.seconds),
186976
+ exitCode: exitCodes.timeout
186977
+ };
186978
+ }
186979
+ await deps.sleep(pollIntervalMs2);
186980
+ }
186981
+ }
186982
+
186983
+ // src/domains/interactiveRunner/collectFiles.ts
186984
+ async function collectRunFiles2(deps) {
186985
+ try {
186986
+ return { files: await deps.collectRunFiles(), ok: true };
186987
+ } catch (error51) {
186988
+ const path9 = readErrorPath(error51);
186989
+ return {
186990
+ error: path9 === undefined ? interactiveRunnerMessages.filesUnreadable(errorMessage(error51)) : interactiveRunnerMessages.fileUnreadable(path9, errorMessage(error51)),
186991
+ ok: false
186992
+ };
186993
+ }
186994
+ }
186995
+ function readErrorPath(error51) {
186996
+ if (typeof error51 !== "object" || error51 === null)
186997
+ return;
186998
+ const path9 = error51.path;
186999
+ return typeof path9 === "string" ? path9 : undefined;
187000
+ }
187001
+
187002
+ // src/domains/interactiveRunner/followRun.ts
187003
+ var pollIntervalMs3 = 1000;
187004
+ function findSettlement(entries) {
187005
+ for (const entry of entries) {
187006
+ const settlement = readRunSettlement(entry.payload);
187007
+ if (settlement.type !== "settled")
187008
+ continue;
187009
+ if (settlement.status === "passed")
187010
+ return { type: "passed" };
187011
+ if (settlement.status === "failed") {
187012
+ return { errorMessage: settlement.errorMessage, type: "failed" };
187013
+ }
187014
+ return { status: settlement.status, type: "unrecognized" };
187015
+ }
187016
+ return;
187017
+ }
187018
+ function reportSettlement(ctx, settlement) {
187019
+ if (settlement.type === "passed") {
187020
+ ctx.ui.success(interactiveRunnerMessages.runPassed);
187021
+ return;
187022
+ }
187023
+ return {
187024
+ error: settlement.type === "failed" ? interactiveRunnerMessages.runFailed(settlement.errorMessage) : interactiveRunnerMessages.runSettledUnknown(settlement.status),
187025
+ exitCode: exitCodes.testFailure
187026
+ };
187027
+ }
187028
+ async function followRun(ctx, options, deps) {
187029
+ const readLogs = createJournalCursor(ctx, options.runnerId, {
187030
+ runId: options.runId,
187031
+ stream: "run-logs"
187032
+ });
187033
+ const readStatus = createJournalCursor(ctx, options.runnerId, {
187034
+ runId: options.runId,
187035
+ stream: "run-status"
187036
+ });
187037
+ const unreachable = createUnreachableBudget(pollIntervalMs3);
187038
+ const printLogs = async () => {
187039
+ const logs = await readLogs();
187040
+ if (logs.type !== "entries")
187041
+ return logs;
187042
+ for (const entry of logs.entries) {
187043
+ ctx.ui.stream(entry, formatRunLogLine(entry.payload));
187044
+ }
187045
+ return logs;
187046
+ };
187047
+ const maxPolls = Math.max(1, Math.ceil(options.timeoutSeconds * 1000 / pollIntervalMs3));
187048
+ for (let poll = 1;; poll++) {
187049
+ const logs = await printLogs();
187050
+ if (logs.type === "failed")
187051
+ return journalReadFailure(logs);
187052
+ const status = logs.type === "unreachable" ? logs : await readStatus();
187053
+ if (status.type === "failed")
187054
+ return journalReadFailure(status);
187055
+ if (logs.type === "unreachable" || status.type === "unreachable") {
187056
+ if (unreachable.exhausted())
187057
+ return { ...unreachableFailure };
187058
+ } else {
187059
+ unreachable.reset();
187060
+ const settlement = findSettlement(status.entries);
187061
+ if (settlement !== undefined) {
187062
+ await printLogs();
187063
+ return reportSettlement(ctx, settlement);
187064
+ }
187065
+ }
187066
+ if (poll >= maxPolls) {
187067
+ return {
187068
+ error: interactiveRunnerMessages.followTimedOut(options.runId, options.runnerId, options.timeoutSeconds),
187069
+ exitCode: exitCodes.timeout
187070
+ };
187071
+ }
187072
+ await deps.sleep(pollIntervalMs3);
187073
+ }
187074
+ }
187075
+
187076
+ // src/domains/interactiveRunner/runFlow.ts
187077
+ async function handleRunnerRun(ctx, options, deps) {
187078
+ const timeout = parseFollowTimeout(options.timeout);
187079
+ if (!timeout.ok) {
187080
+ return { error: timeout.error, exitCode: exitCodes.invalidArgs };
187081
+ }
187082
+ const entryPointPath = toCollectedPath(deps.cwd, options.entryPoint);
187083
+ const collected = await collectRunFiles2(deps);
187084
+ if (!collected.ok) {
187085
+ return { error: collected.error, exitCode: exitCodes.config };
187086
+ }
187087
+ const files = collected.files;
187088
+ const check2 = checkRunFiles(files, entryPointPath);
187089
+ if (check2.type !== "ok") {
187090
+ return {
187091
+ error: describeRunFilesCheck(check2),
187092
+ exitCode: exitCodes.invalidArgs
187093
+ };
187094
+ }
187095
+ const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
187096
+ if (resolved.type === "failed") {
187097
+ return { error: resolved.error, exitCode: resolved.exitCode };
187098
+ }
187099
+ announceRunner(ctx, resolved);
187100
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.runFlow, { entryPointPath, files, id: resolved.runnerId });
187101
+ if (!result.ok) {
187102
+ return { ...failureFields(result), exitCode: exitCodes.network };
187103
+ }
187104
+ switch (result.value.outcome) {
187105
+ case "runner-target-mismatch":
187106
+ return {
187107
+ error: interactiveRunnerMessages.targetMismatch(result.value.runnerName, result.value.requiredRunnerName),
187108
+ exitCode: exitCodes.invalidArgs
187109
+ };
187110
+ case "runner-unreachable":
187111
+ return {
187112
+ error: interactiveRunnerMessages.submitMayHaveStarted,
187113
+ exitCode: exitCodes.network
187114
+ };
187115
+ case "submitted": {
187116
+ const runId = result.value.runId;
187117
+ if (!options.follow) {
187118
+ ctx.ui.output({ runId, runnerId: resolved.runnerId }, interactiveRunnerMessages.runSubmitted(runId));
187119
+ return;
187120
+ }
187121
+ ctx.ui.info(interactiveRunnerMessages.runSubmitted(runId));
187122
+ return followRun(ctx, {
187123
+ runId,
187124
+ runnerId: resolved.runnerId,
187125
+ timeoutSeconds: timeout.seconds
187126
+ }, deps);
187127
+ }
187128
+ default: {
187129
+ result.value;
187130
+ return {
187131
+ error: interactiveRunnerMessages.runSubmitAnsweredUnknown(String(result.value.outcome)),
187132
+ exitCode: exitCodes.network
187133
+ };
187134
+ }
187135
+ }
187136
+ }
187137
+
187138
+ // src/commands/runner/run.register.ts
187139
+ var runExamples2 = `
187140
+ Examples:
187141
+ $ qawolf runner run flows/checkout.flow.ts
187142
+ $ qawolf runner run flows/checkout.flow.ts --follow`;
187143
+ var eventsExamples = `
187144
+ Examples:
187145
+ $ qawolf runner events recorder --tail 5
187146
+ $ qawolf runner events run-logs --run <runId> --follow
187147
+ $ qawolf runner events console --since 120 --json`;
187148
+ function registerRunnerRunCommands(runner, signals) {
187149
+ declareCommandKind(runner.command("run <file>"), "write").description("Run a flow on an interactive runner, shipping the current directory's files with it").option("--follow", "Stream the run's logs until it settles", false).option("--runner <id>", runnerFlagDescription).option("--timeout <seconds>", "Give up following after this long. Following keeps the runner alive, so a run that never settles would otherwise bill until the terminal closed", String(defaultFollowTimeoutSeconds)).addHelpText("after", runExamples2).action((file2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRun(ctx, {
187150
+ entryPoint: file2,
187151
+ follow: opts.follow,
187152
+ runner: opts.runner,
187153
+ timeout: opts.timeout
187154
+ }, runnerDeps(ctx)))(opts, command));
187155
+ declareCommandKind(runner.command("events <stream>"), "read").description(`Print a runner's journal, one entry per line. QA Wolf writes ${knownJournalStreams.join(", ")}`).option("--follow", "Keep reading as new entries arrive. Reading counts as activity, so a follow left open keeps the runner alive and billing", false).option("--run <id>", "Restrict run-scoped streams to one run").option("--runner <id>", runnerFlagDescription).option("--since <sequence>", "Read entries after this sequence").option("--tail <count>", "Read only the newest <count> entries").option("--timeout <seconds>", "Give up following after this long. Reading keeps the runner alive, so a follow left open would otherwise bill until the terminal closed", String(defaultFollowTimeoutSeconds)).addHelpText("after", eventsExamples).action((stream, opts, command) => withAuthContext(signals, (ctx) => handleRunnerEvents(ctx, {
187156
+ envelope: Boolean(command.optsWithGlobals().json),
187157
+ follow: opts.follow,
187158
+ run: opts.run,
187159
+ runner: opts.runner,
187160
+ since: opts.since,
187161
+ stream,
187162
+ tail: opts.tail,
187163
+ timeout: opts.timeout
187164
+ }, runnerDeps(ctx)))(opts, command));
187165
+ }
187166
+
187167
+ // src/commands/runner/index.ts
187168
+ function registerRunnerCommand(program2, signals) {
187169
+ const runner = program2.command("runner").description("Drive an interactive runner on the QA Wolf platform");
187170
+ registerRunnerLifecycleCommands(runner, signals);
187171
+ registerRunnerRunCommands(runner, signals);
187172
+ registerRunnerInteractCommands(runner, signals);
187173
+ }
187174
+
186047
187175
  // src/commands/program.ts
186048
187176
  function createProgram({
186049
187177
  signals
@@ -186056,6 +187184,7 @@ function createProgram({
186056
187184
  registerFlowsCommand(program2, signals);
186057
187185
  registerInitCommand(program2, signals);
186058
187186
  registerInstallCommand(program2, signals);
187187
+ registerRunnerCommand(program2, signals);
186059
187188
  registerPublicApiCommands(program2, signals);
186060
187189
  return program2;
186061
187190
  }
@@ -186079,4 +187208,4 @@ createProgram({ signals }).parseAsync().catch(() => {
186079
187208
  process.exitCode = 1;
186080
187209
  }).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
186081
187210
 
186082
- //# debugId=45F0ABBEFD04330664756E2164756E21
187211
+ //# debugId=B4115D163429543264756E2164756E21