@qawolf/cli 1.16.0 → 1.17.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
@@ -326123,7 +326123,18 @@ var interactMessages = {
326123
326123
  packageInstalled: (name, version) => `Installed ${name}@${version} into the runner's live run.`,
326124
326124
  packageJsonUnreadable: (reason) => `package.json could not be read because ${reason}. Fix it and try again.`,
326125
326125
  installFailed: (name, version, errorMessage2) => `npm could not install ${name}@${version}${errorMessage2 === undefined ? "" : `. ${errorMessage2}`}. Check the name and the version; waiting will not change this.`,
326126
- importAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`
326126
+ importAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
326127
+ highlightCleared: "Cleared the runner's highlight. Its next screenshot shows the page undrawn on.",
326128
+ highlightMatched: (matchCount, targetPage) => `Highlighted ${pluralize(matchCount, "element", "elements")}${targetPage === undefined ? "" : ` on ${targetPage}`}. The highlight stays until it is replaced or cleared, so take a screenshot to see it.`,
326129
+ highlightMatchedNothing: (selector) => `The page read "${selector}" as a selector and nothing matched it, so nothing is highlighted. The selector is well formed, so this is the page rather than the syntax: check what is actually on it with qawolf runner inspect page-html.`,
326130
+ highlightSelectorInvalid: (selector) => `The page could not read "${selector}" as a selector, so nothing was highlighted. This is the syntax rather than the page, and retrying will not change it.`,
326131
+ highlightNoAnswer: "The page did not answer, so nothing is highlighted. A highlight runs inside the page, so a page that is gone or part-way through navigating does not answer at all rather than answering slowly. Take a screenshot to see where the runner is, then try again.",
326132
+ runnerCannotHighlightSelectors: "This runner has no browser to draw on, so there is nothing to highlight. Retrying will never help: launch a playwright runner instead.",
326133
+ highlightAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
326134
+ snapshotPromoted: (screenshotPath, baselinePath) => `Promoted ${screenshotPath} to the baseline at ${baselinePath}.`,
326135
+ snapshotNotFound: (screenshotPath) => `The run wrote no screenshot at "${screenshotPath}", so nothing was changed. The two paths have to be the ones an image diff reported: read them from the image-diff-artifact entry on qawolf runner events run-events.`,
326136
+ runnerCannotPromoteSnapshots: "This runner stores no screenshots, so it holds no baseline to replace. Retrying will never help.",
326137
+ promoteSnapshotAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`
326127
326138
  };
326128
326139
 
326129
326140
  // src/core/messages/interactiveRunner/lifecycle.ts
@@ -326138,7 +326149,9 @@ var lifecycleMessages = {
326138
326149
  launched: (id) => `Launched runner ${id}.`,
326139
326150
  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 terminated or idles out, so terminate it with qawolf runner terminate --runner ${id} when you are done.`,
326140
326151
  noRunnerIdForImport: `${noRunnerId} An install also needs a live run to go into, which a runner gets from qawolf runner run.`,
326152
+ noRunnerIdForHighlight: `${noRunnerId} Highlighting also needs a live page to draw on, which a runner gets from its first run: run a flow on it with qawolf runner run.`,
326141
326153
  noRunnerIdForInspect: `${noRunnerId} Inspecting also needs a page, which a runner gets from its first run: run a flow on it with qawolf runner run.`,
326154
+ noRunnerIdForPromoteSnapshot: `${noRunnerId} A baseline is replaced on the runner that produced the screenshot, so it has to be the runner the diff came from.`,
326142
326155
  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.`,
326143
326156
  noRunnerId,
326144
326157
  wasNotRunning: (id) => `Runner ${id} was not running.`,
@@ -326152,6 +326165,8 @@ var lifecycleMessages = {
326152
326165
  var runMessages = {
326153
326166
  envFileUnparseable: (reason) => `The --env-file could not be read. ${reason}`,
326154
326167
  envFileUnreadable: (path) => `No env file at ${path}. Pass a path that exists, or drop --env-file to send no environment.`,
326168
+ envIdBlank: "--env-id was given nothing. Pass the id or alias of a QA Wolf environment, or drop the flag to send no environment.",
326169
+ envIdWithEnvFile: "--env-id and --env-file both give the run its environment, so only one may be passed. Use --env-id for a QA Wolf environment, which QA Wolf reads itself, or --env-file for a dotenv file on this machine.",
326155
326170
  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.`,
326156
326171
  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.`,
326157
326172
  filesUnreadable: (reason) => `One of the files that travel to a runner could not be read: ${reason}.`,
@@ -342275,7 +342290,7 @@ function refusalForRunEnvironmentVariableName(name) {
342275
342290
  return;
342276
342291
  }
342277
342292
  var maxRunEnvironmentVariables = 200;
342278
- var maxRunEnvironmentValueLength = 8 * 1024;
342293
+ var maxRunEnvironmentValueLength = 16 * 1024;
342279
342294
  var runEnvironmentSchema = exports_external.record(exports_external.string(), exports_external.string().max(maxRunEnvironmentValueLength)).superRefine((environment, refinementContext) => {
342280
342295
  for (const name of Object.keys(environment)) {
342281
342296
  const refusal = refusalForRunEnvironmentVariableName(name);
@@ -342384,6 +342399,49 @@ var makeInspectOnRunnerContract = () => {
342384
342399
  output
342385
342400
  };
342386
342401
  };
342402
+ // node_modules/@qawolf/api-contracts/dist/v1/runner/inspectMobileShapes.js
342403
+ var boundsSchema = exports_external.object({
342404
+ bottom: exports_external.number(),
342405
+ left: exports_external.number(),
342406
+ right: exports_external.number(),
342407
+ top: exports_external.number()
342408
+ });
342409
+ var pointSchema = exports_external.object({ x: exports_external.number(), y: exports_external.number() });
342410
+ var selectorSchema = exports_external.object({
342411
+ strategy: exports_external.union([
342412
+ exports_external.object({
342413
+ name: exports_external.literal("xpath"),
342414
+ type: exports_external.enum(["unique", "full-path", "sibling-of", "parent-of"])
342415
+ }),
342416
+ exports_external.object({ name: exports_external.literal("ios-predicate") }),
342417
+ exports_external.object({
342418
+ hostSelector: exports_external.string(),
342419
+ innerSelector: exports_external.string(),
342420
+ name: exports_external.literal("shadow")
342421
+ })
342422
+ ]),
342423
+ value: exports_external.string()
342424
+ });
342425
+ var elementSchema = exports_external.object({
342426
+ attributes: exports_external.record(exports_external.string(), exports_external.string()),
342427
+ bounds: boundsSchema.optional().describe("The element's on-screen rectangle, absent off-screen."),
342428
+ center: pointSchema.optional().describe("Derived from `bounds`; absent wherever `bounds` is."),
342429
+ selectors: selectorSchema.array().describe("Ways to address this element again, most reliable first."),
342430
+ tag: exports_external.string()
342431
+ });
342432
+ var basePageSourceNodeSchema = exports_external.object({
342433
+ attributes: exports_external.object({
342434
+ bounds: boundsSchema.optional(),
342435
+ rest: exports_external.record(exports_external.string(), exports_external.string()).optional()
342436
+ }).optional(),
342437
+ selectors: selectorSchema.array(),
342438
+ tag: exports_external.string()
342439
+ });
342440
+ var pageSourceNodeSchema = basePageSourceNodeSchema.extend({
342441
+ children: exports_external.lazy(() => exports_external.union([pageSourceNodeSchema, exports_external.string()]).array()).optional().describe("A string child is a text node; there is no element to select.")
342442
+ });
342443
+ var orientationSchema = exports_external.enum(["PORTRAIT", "LANDSCAPE"]);
342444
+
342387
342445
  // node_modules/@qawolf/api-contracts/dist/v1/runner/outcome.js
342388
342446
  function makeRunnerFailureSchema(reasons) {
342389
342447
  return exports_external.object({
@@ -342392,6 +342450,100 @@ function makeRunnerFailureSchema(reasons) {
342392
342450
  });
342393
342451
  }
342394
342452
 
342453
+ // node_modules/@qawolf/api-contracts/dist/v1/runner/inspectMobile.js
342454
+ var maxElementTextLength = 500;
342455
+ var inspectMobileRequestSchema = exports_external.discriminatedUnion("what", [
342456
+ exports_external.object({
342457
+ what: exports_external.literal("session").describe("The status of the Appium session driving the device: ready and what it is, or why not.")
342458
+ }),
342459
+ exports_external.object({
342460
+ what: exports_external.literal("contexts").describe("The WebView contexts available on the device, and which one is current.")
342461
+ }),
342462
+ exports_external.object({
342463
+ context: exports_external.string().min(1).optional().describe("Read this context instead of whichever is current."),
342464
+ what: exports_external.literal("page").describe("The page source of the device's current context, as a tree.")
342465
+ }),
342466
+ exports_external.discriminatedUnion("by", [
342467
+ exports_external.object({
342468
+ by: exports_external.literal("point"),
342469
+ context: exports_external.string().min(1).optional(),
342470
+ what: exports_external.literal("elements"),
342471
+ x: exports_external.int().min(0).describe("Whole pixels on the device's own screen."),
342472
+ y: exports_external.int().min(0).describe("Whole pixels on the device's own screen.")
342473
+ }),
342474
+ exports_external.object({
342475
+ by: exports_external.literal("text"),
342476
+ context: exports_external.string().min(1).optional(),
342477
+ partial: exports_external.boolean().optional().describe("Match text containing this, rather than exactly this."),
342478
+ text: exports_external.string().min(1).max(maxElementTextLength),
342479
+ what: exports_external.literal("elements")
342480
+ })
342481
+ ]).describe("Elements at a screen point, or elements carrying some text — never both at once.")
342482
+ ]);
342483
+ var inspectMobileAnswerSchema = exports_external.discriminatedUnion("what", [
342484
+ exports_external.object({
342485
+ outcome: exports_external.literal("success"),
342486
+ session: exports_external.discriminatedUnion("type", [
342487
+ exports_external.object({
342488
+ deviceName: exports_external.string().optional(),
342489
+ platformName: exports_external.string(),
342490
+ sessionId: exports_external.string(),
342491
+ type: exports_external.literal("ready")
342492
+ }),
342493
+ exports_external.object({
342494
+ error: exports_external.string(),
342495
+ type: exports_external.literal("unreachable")
342496
+ }),
342497
+ exports_external.object({
342498
+ sessionCount: exports_external.number(),
342499
+ type: exports_external.literal("ambiguous")
342500
+ }),
342501
+ exports_external.object({ type: exports_external.literal("no-session") })
342502
+ ]),
342503
+ what: exports_external.literal("session")
342504
+ }),
342505
+ exports_external.object({
342506
+ contexts: exports_external.string().array(),
342507
+ current: exports_external.string(),
342508
+ outcome: exports_external.literal("success"),
342509
+ what: exports_external.literal("contexts")
342510
+ }),
342511
+ exports_external.object({
342512
+ context: exports_external.string(),
342513
+ orientation: orientationSchema,
342514
+ outcome: exports_external.literal("success"),
342515
+ pageSource: pageSourceNodeSchema,
342516
+ what: exports_external.literal("page")
342517
+ }),
342518
+ exports_external.object({
342519
+ matches: elementSchema.array(),
342520
+ outcome: exports_external.literal("success"),
342521
+ what: exports_external.literal("elements")
342522
+ })
342523
+ ]);
342524
+ var makeInspectMobileOnRunnerContract = () => {
342525
+ const input = exports_external.object({
342526
+ id: runnerIdSchema.describe("Id of the runner to inspect."),
342527
+ request: inspectMobileRequestSchema
342528
+ });
342529
+ const output = exports_external.discriminatedUnion("outcome", [
342530
+ inspectMobileAnswerSchema,
342531
+ makeRunnerFailureSchema([
342532
+ "runner-is-not-mobile",
342533
+ "screen-needs-a-run",
342534
+ "screen-not-ready",
342535
+ runnerUnreachableFailureReason
342536
+ ])
342537
+ ]);
342538
+ return {
342539
+ description: `Inspect one thing on a mobile interactive runner: the Appium session's status, the WebView contexts available, the current context's page source, or the elements at a point or carrying some text. Mobile only — a browser runner answers \`runner-is-not-mobile\`; call \`runner.inspect\` for a browser's equivalent surface instead. \`what: "session"\` always answers with the session's own status rather than \`screen-needs-a-run\`, since that is the question it exists to answer; the other three request kinds need a live session first and answer the same \`screen-needs-a-run\` or \`screen-not-ready\` outcomes \`runner.performAction\` and \`runner.takeScreenshot\` use, instead of reading anything when there is none. \`screen-needs-a-run\` means no Appium session has started on this runner yet — call \`runner.runFlow\` with a flow that opens one, then inspect again. \`screen-not-ready\` means the runner's Appium session exists but did not answer this instant, or more than one is somehow live — retry once; if it persists, relaunch the runner. \`runner-is-not-mobile\` means there is nothing here to inspect, and retrying will never help — launch an \`android\` or \`ios\` runner instead. ${retryableRunnerUnreachableDescription}`,
342540
+ input,
342541
+ kind: "read",
342542
+ name: "runner.inspectMobile",
342543
+ output
342544
+ };
342545
+ };
342546
+
342395
342547
  // node_modules/@qawolf/api-contracts/dist/v1/runner/journal.js
342396
342548
  var journalEntrySchema = exports_external.object({
342397
342549
  payload: exports_external.unknown(),
@@ -343090,6 +343242,43 @@ var makeEvaluateSnippetOnRunnerContract = () => {
343090
343242
  };
343091
343243
  };
343092
343244
 
343245
+ // node_modules/@qawolf/api-contracts/dist/v1/runner/highlightSelector.js
343246
+ var maxHighlightSelectorLength = 2000;
343247
+ var makeHighlightSelectorOnRunnerContract = () => {
343248
+ const input = exports_external.object({
343249
+ id: runnerIdSchema.describe("Id of the runner to highlight on."),
343250
+ selector: exports_external.string().max(maxHighlightSelectorLength).describe("The selector to highlight. An empty string clears whatever is highlighted.")
343251
+ });
343252
+ const output = exports_external.discriminatedUnion("outcome", [
343253
+ exports_external.object({
343254
+ matchCount: exports_external.number().int().gte(0).describe("How many elements the selector matched."),
343255
+ outcome: exports_external.literal("success"),
343256
+ selector: exports_external.string().describe("The selector the page matched on."),
343257
+ status: exports_external.literal(["valid", "invalid", "empty"]).describe("`valid` when the selector parsed and matched, `empty` when it parsed and matched nothing, `invalid` when the page could not read it as a selector."),
343258
+ targetPage: exports_external.string().optional().describe("Present when the match was on a page other than the default one.")
343259
+ }),
343260
+ exports_external.object({
343261
+ outcome: exports_external.literal("cleared")
343262
+ }),
343263
+ exports_external.object({
343264
+ errorMessage: exports_external.string().optional(),
343265
+ failureReason: exports_external.enum([
343266
+ "no-answer",
343267
+ "runner-cannot-highlight-selectors",
343268
+ "runner-unreachable"
343269
+ ]),
343270
+ outcome: exports_external.literal("failure")
343271
+ })
343272
+ ]);
343273
+ return {
343274
+ description: "Highlight the elements a selector matches on an interactive runner's live page, and answer how many it matched. The highlight stays until it is replaced or cleared, so it is visible in the next `runner.takeScreenshot` — which is the point, since a caller cannot see the runner's screen otherwise. Send an empty selector to clear, which answers `cleared`. `status` tells a selector that matched nothing (`empty`) from one the page could not parse (`invalid`), so a caller can tell a bad locator from a locator pointing at nothing. `no-answer` if the page did not answer in time: the highlight runs inside the page, so a page that is gone or mid-navigation does not answer slowly, it does not answer at all. `runner-cannot-highlight-selectors` on a runner with no browser to draw on. `runner-unreachable` if the runner could not be reached: it may still be starting, or it may have terminated after inactivity.",
343275
+ input,
343276
+ kind: "write",
343277
+ name: "runner.highlightSelector",
343278
+ output
343279
+ };
343280
+ };
343281
+
343093
343282
  // node_modules/@qawolf/api-contracts/dist/v1/runner/importPackage.js
343094
343283
  var maxPackageNameLength = 214;
343095
343284
  var maxPackageVersionLength = 256;
@@ -343153,6 +343342,34 @@ var makeTerminateRunnerContract = () => {
343153
343342
  };
343154
343343
  };
343155
343344
 
343345
+ // node_modules/@qawolf/api-contracts/dist/v1/runner/promoteSnapshot.js
343346
+ var maxSnapshotPathLength = 1024;
343347
+ var makePromoteSnapshotOnRunnerContract = () => {
343348
+ const input = exports_external.object({
343349
+ baselinePath: exports_external.string().min(1).max(maxSnapshotPathLength).describe("The baseline to replace, as named in the image diff."),
343350
+ id: runnerIdSchema.describe("Id of the runner holding the screenshot."),
343351
+ screenshotPath: exports_external.string().min(1).max(maxSnapshotPathLength).describe("The screenshot to promote, as named in the image diff.")
343352
+ });
343353
+ const output = exports_external.discriminatedUnion("outcome", [
343354
+ exports_external.object({ outcome: exports_external.literal("success") }),
343355
+ exports_external.object({
343356
+ failureReason: exports_external.enum([
343357
+ "runner-cannot-promote-snapshots",
343358
+ "runner-unreachable",
343359
+ "snapshot-not-found"
343360
+ ]),
343361
+ outcome: exports_external.literal("failure")
343362
+ })
343363
+ ]);
343364
+ return {
343365
+ description: "Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it. The two paths are the ones the diff reported, which reach a caller as an `image-diff-artifact` entry on the runner's `run-events` journal stream. `snapshot-not-found` if the run wrote no screenshot at that path, which usually means the paths were not taken from a diff this run produced; nothing is changed, so correcting the path and repeating is safe. `runner-cannot-promote-snapshots` on a runner that stores no screenshots. `runner-unreachable` if the runner could not be reached: it may still be starting, or it may have terminated after inactivity. A promotion that lands is not undone by promoting again, so a retry after an unreachable answer is safe.",
343366
+ input,
343367
+ kind: "write",
343368
+ name: "runner.promoteSnapshot",
343369
+ output
343370
+ };
343371
+ };
343372
+
343156
343373
  // node_modules/@qawolf/api-contracts/dist/v1/runner/readJournal.js
343157
343374
  var makeReadRunnerJournalContract = () => {
343158
343375
  const input = readJournalRequestSchema.extend({
@@ -343184,14 +343401,18 @@ var unchangedFilesSchema = exports_external.record(exports_external.string(), ex
343184
343401
  });
343185
343402
  }
343186
343403
  });
343187
- var makeRunFlowOnRunnerContract = () => {
343404
+ var makeRunFlowOnRunnerContract = (ids) => {
343188
343405
  const input = exports_external.object({
343189
343406
  entryPointPath: runFilePathSchema.describe("Path of the flow file to run, as it appears in `files`."),
343190
343407
  env: runEnvironmentSchema.optional().describe("Environment variables to make available to the run."),
343408
+ environmentId: ids.environmentRef.optional().describe("A QA Wolf environment whose variables the run is given, by id or alias. QA Wolf reads and decrypts them itself, so they never travel in the request and the count and length limits on `env` do not apply to them. Send this or `env`, not both."),
343191
343409
  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."),
343192
343410
  id: runnerIdSchema.describe("Id of the runner to run the flow on."),
343193
343411
  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."),
343194
343412
  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.")
343413
+ }).refine((request) => request.env === undefined || request.environmentId === undefined, {
343414
+ 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.",
343415
+ path: ["environmentId"]
343195
343416
  }).refine((request) => Object.hasOwn(request.files, request.entryPointPath), {
343196
343417
  error: "The entry point must be one of the files.",
343197
343418
  path: ["files"]
@@ -343342,12 +343563,15 @@ var makeContractsV1 = (ids) => {
343342
343563
  },
343343
343564
  runner: {
343344
343565
  evaluateSnippet: makeEvaluateSnippetOnRunnerContract(),
343566
+ highlightSelector: makeHighlightSelectorOnRunnerContract(),
343345
343567
  importPackage: makeImportPackageOnRunnerContract(),
343346
343568
  inspect: makeInspectOnRunnerContract(),
343569
+ inspectMobile: makeInspectMobileOnRunnerContract(),
343347
343570
  launch: makeLaunchRunnerContract(),
343348
343571
  performAction: makePerformActionOnRunnerContract(),
343572
+ promoteSnapshot: makePromoteSnapshotOnRunnerContract(),
343349
343573
  readJournal: makeReadRunnerJournalContract(),
343350
- runFlow: makeRunFlowOnRunnerContract(),
343574
+ runFlow: makeRunFlowOnRunnerContract(resolvedIds),
343351
343575
  stopRun: makeStopRunOnRunnerContract(),
343352
343576
  takeScreenshot: makeTakeScreenshotOnRunnerContract(),
343353
343577
  terminate: makeTerminateRunnerContract()
@@ -345874,7 +346098,7 @@ function startUpdateCheck(deps) {
345874
346098
  // package.json
345875
346099
  var package_default = {
345876
346100
  name: "@qawolf/cli",
345877
- version: "1.16.0",
346101
+ version: "1.17.0",
345878
346102
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
345879
346103
  keywords: [
345880
346104
  "automation",
@@ -345934,7 +346158,7 @@ var package_default = {
345934
346158
  "@clack/prompts": "1.5.1",
345935
346159
  "@napi-rs/keyring": "1.3.0",
345936
346160
  "@oxc-node/core": "0.1.0",
345937
- "@qawolf/api-contracts": "0.32.0",
346161
+ "@qawolf/api-contracts": "0.34.0",
345938
346162
  "@qawolf/emails": "1.1.1",
345939
346163
  "@qawolf/flow-targets": "1.0.0",
345940
346164
  "@qawolf/flows": "0.1.4",
@@ -356368,10 +356592,13 @@ async function handlePublicApiCommand(ctx, spec, options) {
356368
356592
  var skippedContractNames = new Set([
356369
356593
  "flow.list",
356370
356594
  "runner.evaluateSnippet",
356595
+ "runner.highlightSelector",
356371
356596
  "runner.importPackage",
356372
356597
  "runner.inspect",
356598
+ "runner.inspectMobile",
356373
356599
  "runner.launch",
356374
356600
  "runner.performAction",
356601
+ "runner.promoteSnapshot",
356375
356602
  "runner.readJournal",
356376
356603
  "runner.runFlow",
356377
356604
  "runner.stopRun",
@@ -356430,45 +356657,6 @@ function registerPublicApiCommands(program2, signals, options = {}) {
356430
356657
  }
356431
356658
  }
356432
356659
 
356433
- // src/domains/interactiveRunner/importPackage.ts
356434
- import { join as join44 } from "node:path";
356435
-
356436
- // src/core/interactiveRunner/npmDependencies.ts
356437
- var sectionsInPrecedenceOrder = ["dependencies", "devDependencies"];
356438
- var isInstallable = (version2) => !version2.startsWith("workspace:");
356439
- function isRecord5(value) {
356440
- return typeof value === "object" && value !== null && !Array.isArray(value);
356441
- }
356442
- function readNpmDependencies(packageJsonContent) {
356443
- let parsed;
356444
- try {
356445
- parsed = JSON.parse(packageJsonContent);
356446
- } catch {
356447
- return { ok: false, reason: "it is not valid JSON" };
356448
- }
356449
- if (!isRecord5(parsed))
356450
- return { ok: false, reason: "it is not an object" };
356451
- const dependencies = {};
356452
- for (const section of sectionsInPrecedenceOrder) {
356453
- const declared = parsed[section];
356454
- if (declared === undefined)
356455
- continue;
356456
- if (!isRecord5(declared)) {
356457
- return { ok: false, reason: `${section} is not an object` };
356458
- }
356459
- for (const [name, version2] of Object.entries(declared)) {
356460
- if (typeof version2 !== "string") {
356461
- return { ok: false, reason: `${section}.${name} is not a string` };
356462
- }
356463
- if (Object.hasOwn(dependencies, name))
356464
- continue;
356465
- if (isInstallable(version2))
356466
- dependencies[name] = version2;
356467
- }
356468
- }
356469
- return { dependencies, ok: true };
356470
- }
356471
-
356472
356660
  // src/domains/interactiveRunner/runnerCallOptions.ts
356473
356661
  var runnerCallOptions = { timeoutMs: 90000 };
356474
356662
 
@@ -356583,71 +356771,73 @@ function announceRunner(ctx, resolved) {
356583
356771
  }
356584
356772
  }
356585
356773
 
356586
- // src/domains/interactiveRunner/importPackage.ts
356587
- var defaultPackageVersion = "latest";
356588
- async function handleRunnerImportPackage(ctx, options, deps) {
356589
- const content = await deps.readFile(join44(deps.cwd, runPackageJsonPath)).catch(() => {
356590
- return;
356591
- });
356592
- if (content === undefined) {
356593
- return {
356594
- error: interactiveRunnerMessages.missingPackageJsonForImport,
356595
- exitCode: exitCodes.config
356596
- };
356597
- }
356598
- const dependencies = readNpmDependencies(content);
356599
- if (!dependencies.ok) {
356600
- return {
356601
- error: interactiveRunnerMessages.packageJsonUnreadable(dependencies.reason),
356602
- exitCode: exitCodes.config
356603
- };
356604
- }
356774
+ // src/domains/interactiveRunner/highlightSelector.ts
356775
+ async function handleRunnerHighlightSelector(ctx, options, deps) {
356605
356776
  const resolved = await resolveRunner(ctx, {
356606
356777
  autoLaunch: false,
356607
- noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForImport,
356778
+ noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForHighlight,
356608
356779
  runner: options.runner
356609
356780
  }, deps);
356610
356781
  if (resolved.type === "failed") {
356611
356782
  return { ...failureFields(resolved), exitCode: resolved.exitCode };
356612
356783
  }
356613
- const packageVersion = options.version ?? defaultPackageVersion;
356614
- const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.importPackage, {
356615
- id: resolved.runnerId,
356616
- npmDependencies: dependencies.dependencies,
356617
- packageName: options.name,
356618
- packageVersion
356619
- }, runnerCallOptions);
356784
+ const selector = options.selector ?? "";
356785
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.highlightSelector, { id: resolved.runnerId, selector }, runnerCallOptions);
356620
356786
  if (!result.ok) {
356621
356787
  return { ...failureFields(result), exitCode: exitCodes.network };
356622
356788
  }
356623
- if (result.value.outcome === "failure") {
356624
- const failure = result.value;
356625
- const { failureReason } = failure;
356626
- switch (failureReason) {
356627
- case "install-failed":
356789
+ const { outcome } = result.value;
356790
+ switch (outcome) {
356791
+ case "cleared":
356792
+ ctx.ui.output(result.value, interactiveRunnerMessages.highlightCleared);
356793
+ return;
356794
+ case "success": {
356795
+ const answer = result.value;
356796
+ if (answer.status === "invalid") {
356628
356797
  return {
356629
- error: interactiveRunnerMessages.installFailed(options.name, packageVersion, failure.errorMessage),
356798
+ error: interactiveRunnerMessages.highlightSelectorInvalid(answer.selector),
356630
356799
  exitCode: exitCodes.invalidArgs
356631
356800
  };
356632
- case "runner-unreachable":
356633
- return {
356634
- error: interactiveRunnerMessages.runnerUnreachable,
356635
- exitCode: exitCodes.network
356636
- };
356637
- default: {
356638
- return {
356639
- error: interactiveRunnerMessages.importAnsweredUnknown(failureReason),
356640
- exitCode: exitCodes.network
356641
- };
356642
356801
  }
356802
+ ctx.ui.output(answer, answer.status === "empty" ? interactiveRunnerMessages.highlightMatchedNothing(answer.selector) : interactiveRunnerMessages.highlightMatched(answer.matchCount, answer.targetPage));
356803
+ return;
356643
356804
  }
356805
+ case "failure":
356806
+ return describeFailure(result.value.failureReason);
356807
+ default:
356808
+ return {
356809
+ error: interactiveRunnerMessages.highlightAnsweredUnknown(outcome),
356810
+ exitCode: exitCodes.network
356811
+ };
356812
+ }
356813
+ }
356814
+ function describeFailure(failureReason) {
356815
+ switch (failureReason) {
356816
+ case "no-answer":
356817
+ return {
356818
+ error: interactiveRunnerMessages.highlightNoAnswer,
356819
+ exitCode: exitCodes.network
356820
+ };
356821
+ case "runner-cannot-highlight-selectors":
356822
+ return {
356823
+ error: interactiveRunnerMessages.runnerCannotHighlightSelectors,
356824
+ exitCode: exitCodes.invalidArgs
356825
+ };
356826
+ case "runner-unreachable":
356827
+ return {
356828
+ error: interactiveRunnerMessages.runnerUnreachable,
356829
+ exitCode: exitCodes.network
356830
+ };
356831
+ default:
356832
+ return {
356833
+ error: interactiveRunnerMessages.highlightAnsweredUnknown(failureReason),
356834
+ exitCode: exitCodes.network
356835
+ };
356644
356836
  }
356645
- ctx.ui.output(result.value, interactiveRunnerMessages.packageInstalled(options.name, packageVersion));
356646
- return;
356647
356837
  }
356648
356838
 
356649
356839
  // src/shell/interactiveRunner/collectRunFiles.ts
356650
- import { join as join46 } from "node:path";
356840
+ import { join as join45 } from "node:path";
356651
356841
 
356652
356842
  // src/core/interactiveRunner/getImports.ts
356653
356843
  function getImports(options) {
@@ -356686,14 +356876,14 @@ function isPathAlias(importPath, paths) {
356686
356876
  }
356687
356877
 
356688
356878
  // src/core/interactiveRunner/resolveImportPath.ts
356689
- import { dirname as dirname15, join as join45, normalize as normalize3 } from "node:path/posix";
356879
+ import { dirname as dirname15, join as join44, normalize as normalize3 } from "node:path/posix";
356690
356880
 
356691
356881
  // src/core/interactiveRunner/tsconfigPaths.ts
356692
- function isRecord6(value) {
356882
+ function isRecord5(value) {
356693
356883
  return typeof value === "object" && value !== null;
356694
356884
  }
356695
356885
  function isTsconfigPaths(value) {
356696
- return isRecord6(value) && Object.values(value).every((targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string"));
356886
+ return isRecord5(value) && Object.values(value).every((targets) => Array.isArray(targets) && targets.every((target) => typeof target === "string"));
356697
356887
  }
356698
356888
  function parseTsconfigPaths(tsconfigContent) {
356699
356889
  let parsed;
@@ -356702,10 +356892,10 @@ function parseTsconfigPaths(tsconfigContent) {
356702
356892
  } catch {
356703
356893
  return;
356704
356894
  }
356705
- if (!isRecord6(parsed))
356895
+ if (!isRecord5(parsed))
356706
356896
  return;
356707
356897
  const compilerOptions = parsed["compilerOptions"];
356708
- if (!isRecord6(compilerOptions))
356898
+ if (!isRecord5(compilerOptions))
356709
356899
  return;
356710
356900
  const paths = compilerOptions["paths"];
356711
356901
  return isTsconfigPaths(paths) ? paths : undefined;
@@ -356739,7 +356929,7 @@ function candidatesForExplicitExtension(options) {
356739
356929
  }
356740
356930
  function resolveImportPath(options) {
356741
356931
  const aliasTarget = resolvePathAlias(options.importPath, options.tsconfigPaths);
356742
- const resolvedPath = aliasTarget === undefined ? normalize3(join45(dirname15(options.importingFilePath), options.importPath)) : normalize3(aliasTarget);
356932
+ const resolvedPath = aliasTarget === undefined ? normalize3(join44(dirname15(options.importingFilePath), options.importPath)) : normalize3(aliasTarget);
356743
356933
  const hasExplicitExtension = supportedExtensions.some((extension) => options.importPath.endsWith(extension));
356744
356934
  const candidates = hasExplicitExtension ? candidatesForExplicitExtension({
356745
356935
  importPath: options.importPath,
@@ -356763,7 +356953,7 @@ function resolveImportPath(options) {
356763
356953
  var tsconfigPath = "tsconfig.json";
356764
356954
  async function collectRunFiles(options) {
356765
356955
  const held = await listShippablePaths(options);
356766
- const readFile2 = (path9) => options.fs.readFile(join46(options.cwd, path9));
356956
+ const readFile2 = (path9) => options.fs.readFile(join45(options.cwd, path9));
356767
356957
  const tsconfigContent = held.has(tsconfigPath) ? await readFile2(tsconfigPath).catch(() => {
356768
356958
  return;
356769
356959
  }) : undefined;
@@ -356846,7 +357036,7 @@ function makeRunnerId() {
356846
357036
  }
356847
357037
 
356848
357038
  // src/shell/interactiveRunner/runFilesManifest.ts
356849
- import { join as join47 } from "node:path";
357039
+ import { join as join46 } from "node:path";
356850
357040
  var manifestFileName = "runner-files.json";
356851
357041
  var manifestSchema2 = exports_external.object({
356852
357042
  files: exports_external.array(exports_external.object({ contentHash: exports_external.string(), path: exports_external.string() })),
@@ -356854,8 +357044,8 @@ var manifestSchema2 = exports_external.object({
356854
357044
  version: exports_external.literal(1)
356855
357045
  });
356856
357046
  function makeRunFilesManifestStore(options) {
356857
- const directory = join47(options.cwd, qawolfDir);
356858
- const path9 = join47(directory, manifestFileName);
357047
+ const directory = join46(options.cwd, qawolfDir);
357048
+ const path9 = join46(directory, manifestFileName);
356859
357049
  let pendingWrites = 0;
356860
357050
  return {
356861
357051
  async read() {
@@ -356885,7 +357075,7 @@ function parseJson(text3) {
356885
357075
  }
356886
357076
 
356887
357077
  // src/shell/interactiveRunner/runnerStore.ts
356888
- import { join as join48 } from "node:path";
357078
+ import { join as join47 } from "node:path";
356889
357079
  var storeFileName = "runner.json";
356890
357080
  var storeSchema = exports_external.object({ defaultRunnerId: exports_external.string().optional() });
356891
357081
  function parseJson2(text3) {
@@ -356896,8 +357086,8 @@ function parseJson2(text3) {
356896
357086
  }
356897
357087
  }
356898
357088
  function makeRunnerStore(options) {
356899
- const directory = join48(options.cwd, qawolfDir);
356900
- const path9 = join48(directory, storeFileName);
357089
+ const directory = join47(options.cwd, qawolfDir);
357090
+ const path9 = join47(directory, storeFileName);
356901
357091
  let pendingWrites = 0;
356902
357092
  const nextPendingPath = () => `${path9}.${process.pid}.${++pendingWrites}.tmp`;
356903
357093
  return {
@@ -356979,6 +357169,118 @@ function runnerDeps(ctx) {
356979
357169
  });
356980
357170
  }
356981
357171
 
357172
+ // src/commands/runner/highlightSelector.register.ts
357173
+ var highlightExamples = `
357174
+ Examples:
357175
+ $ qawolf runner highlight-selector "text=Sign in"
357176
+ $ qawolf runner highlight-selector "#checkout" && qawolf runner screenshot
357177
+ $ qawolf runner highlight-selector`;
357178
+ function registerRunnerHighlightSelectorCommand(runner, signals) {
357179
+ declareCommandKind(runner.command("highlight-selector [selector]"), "write").description("Highlight what a selector matches on a runner's live page, so the next screenshot shows it. Omit the selector to clear the highlight").option("--runner <id>", runnerFlagDescription).addHelpText("after", highlightExamples).action((selector, opts, command) => withAuthContext(signals, (ctx) => handleRunnerHighlightSelector(ctx, { runner: opts.runner, selector }, runnerDeps(ctx)))(opts, command));
357180
+ }
357181
+
357182
+ // src/domains/interactiveRunner/importPackage.ts
357183
+ import { join as join48 } from "node:path";
357184
+
357185
+ // src/core/interactiveRunner/npmDependencies.ts
357186
+ var sectionsInPrecedenceOrder = ["dependencies", "devDependencies"];
357187
+ var isInstallable = (version2) => !version2.startsWith("workspace:");
357188
+ function isRecord6(value) {
357189
+ return typeof value === "object" && value !== null && !Array.isArray(value);
357190
+ }
357191
+ function readNpmDependencies(packageJsonContent) {
357192
+ let parsed;
357193
+ try {
357194
+ parsed = JSON.parse(packageJsonContent);
357195
+ } catch {
357196
+ return { ok: false, reason: "it is not valid JSON" };
357197
+ }
357198
+ if (!isRecord6(parsed))
357199
+ return { ok: false, reason: "it is not an object" };
357200
+ const dependencies = {};
357201
+ for (const section of sectionsInPrecedenceOrder) {
357202
+ const declared = parsed[section];
357203
+ if (declared === undefined)
357204
+ continue;
357205
+ if (!isRecord6(declared)) {
357206
+ return { ok: false, reason: `${section} is not an object` };
357207
+ }
357208
+ for (const [name, version2] of Object.entries(declared)) {
357209
+ if (typeof version2 !== "string") {
357210
+ return { ok: false, reason: `${section}.${name} is not a string` };
357211
+ }
357212
+ if (Object.hasOwn(dependencies, name))
357213
+ continue;
357214
+ if (isInstallable(version2))
357215
+ dependencies[name] = version2;
357216
+ }
357217
+ }
357218
+ return { dependencies, ok: true };
357219
+ }
357220
+
357221
+ // src/domains/interactiveRunner/importPackage.ts
357222
+ var defaultPackageVersion = "latest";
357223
+ async function handleRunnerImportPackage(ctx, options, deps) {
357224
+ const content = await deps.readFile(join48(deps.cwd, runPackageJsonPath)).catch(() => {
357225
+ return;
357226
+ });
357227
+ if (content === undefined) {
357228
+ return {
357229
+ error: interactiveRunnerMessages.missingPackageJsonForImport,
357230
+ exitCode: exitCodes.config
357231
+ };
357232
+ }
357233
+ const dependencies = readNpmDependencies(content);
357234
+ if (!dependencies.ok) {
357235
+ return {
357236
+ error: interactiveRunnerMessages.packageJsonUnreadable(dependencies.reason),
357237
+ exitCode: exitCodes.config
357238
+ };
357239
+ }
357240
+ const resolved = await resolveRunner(ctx, {
357241
+ autoLaunch: false,
357242
+ noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForImport,
357243
+ runner: options.runner
357244
+ }, deps);
357245
+ if (resolved.type === "failed") {
357246
+ return { ...failureFields(resolved), exitCode: resolved.exitCode };
357247
+ }
357248
+ const packageVersion = options.version ?? defaultPackageVersion;
357249
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.importPackage, {
357250
+ id: resolved.runnerId,
357251
+ npmDependencies: dependencies.dependencies,
357252
+ packageName: options.name,
357253
+ packageVersion
357254
+ }, runnerCallOptions);
357255
+ if (!result.ok) {
357256
+ return { ...failureFields(result), exitCode: exitCodes.network };
357257
+ }
357258
+ if (result.value.outcome === "failure") {
357259
+ const failure = result.value;
357260
+ const { failureReason } = failure;
357261
+ switch (failureReason) {
357262
+ case "install-failed":
357263
+ return {
357264
+ error: interactiveRunnerMessages.installFailed(options.name, packageVersion, failure.errorMessage),
357265
+ exitCode: exitCodes.invalidArgs
357266
+ };
357267
+ case "runner-unreachable":
357268
+ return {
357269
+ error: interactiveRunnerMessages.runnerUnreachable,
357270
+ exitCode: exitCodes.network
357271
+ };
357272
+ default: {
357273
+ return {
357274
+ error: interactiveRunnerMessages.importAnsweredUnknown(failureReason),
357275
+ exitCode: exitCodes.network
357276
+ };
357277
+ }
357278
+ }
357279
+ }
357280
+ ctx.ui.output(result.value, interactiveRunnerMessages.packageInstalled(options.name, packageVersion));
357281
+ return;
357282
+ }
357283
+
356982
357284
  // src/commands/runner/importPackage.register.ts
356983
357285
  var importPackageExamples = `
356984
357286
  Examples:
@@ -357554,6 +357856,67 @@ function registerRunnerLifecycleCommands(runner, signals) {
357554
357856
  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));
357555
357857
  }
357556
357858
 
357859
+ // src/domains/interactiveRunner/promoteSnapshot.ts
357860
+ async function handleRunnerPromoteSnapshot(ctx, options, deps) {
357861
+ const resolved = await resolveRunner(ctx, {
357862
+ autoLaunch: false,
357863
+ noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForPromoteSnapshot,
357864
+ runner: options.runner
357865
+ }, deps);
357866
+ if (resolved.type === "failed") {
357867
+ return { ...failureFields(resolved), exitCode: resolved.exitCode };
357868
+ }
357869
+ const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.promoteSnapshot, {
357870
+ baselinePath: options.baselinePath,
357871
+ id: resolved.runnerId,
357872
+ screenshotPath: options.screenshotPath
357873
+ }, runnerCallOptions);
357874
+ if (!result.ok) {
357875
+ return { ...failureFields(result), exitCode: exitCodes.network };
357876
+ }
357877
+ if (result.value.outcome === "success") {
357878
+ ctx.ui.output(result.value, interactiveRunnerMessages.snapshotPromoted(options.screenshotPath, options.baselinePath));
357879
+ return;
357880
+ }
357881
+ const { failureReason } = result.value;
357882
+ switch (failureReason) {
357883
+ case "snapshot-not-found":
357884
+ return {
357885
+ error: interactiveRunnerMessages.snapshotNotFound(options.screenshotPath),
357886
+ exitCode: exitCodes.invalidArgs
357887
+ };
357888
+ case "runner-cannot-promote-snapshots":
357889
+ return {
357890
+ error: interactiveRunnerMessages.runnerCannotPromoteSnapshots,
357891
+ exitCode: exitCodes.invalidArgs
357892
+ };
357893
+ case "runner-unreachable":
357894
+ return {
357895
+ error: interactiveRunnerMessages.runnerUnreachable,
357896
+ exitCode: exitCodes.network
357897
+ };
357898
+ default: {
357899
+ return {
357900
+ error: interactiveRunnerMessages.promoteSnapshotAnsweredUnknown(failureReason),
357901
+ exitCode: exitCodes.network
357902
+ };
357903
+ }
357904
+ }
357905
+ }
357906
+
357907
+ // src/commands/runner/promoteSnapshot.register.ts
357908
+ var promoteSnapshotExamples = `
357909
+ Examples:
357910
+ $ qawolf runner promote-snapshot --screenshot checkout-1-actual.png --baseline checkout-1.png
357911
+ $ qawolf runner events run-events --tail 20 | jq 'select(.type == "image-diff-artifact")'`;
357912
+ function registerRunnerPromoteSnapshotCommand(runner, signals) {
357913
+ declareCommandKind(runner.command("promote-snapshot"), "write").description("Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it").requiredOption("--screenshot <path>", "The screenshot to promote, as the image diff named it").requiredOption("--baseline <path>", "The baseline to replace, as the image diff named it").option("--runner <id>", runnerFlagDescription).addHelpText("after", promoteSnapshotExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerPromoteSnapshot(ctx, {
357914
+ baselinePath: opts.baseline,
357915
+ runner: opts.runner,
357916
+ screenshotPath: opts.screenshot
357917
+ }, runnerDeps(ctx)))(opts, command));
357918
+ }
357919
+
357557
357920
  // src/core/interactiveRunner/followTimeout.ts
357558
357921
  var defaultFollowTimeoutSeconds = 3600;
357559
357922
  var followTimeoutSchema = exports_external.coerce.number().int().positive();
@@ -357942,6 +358305,13 @@ var refused = (error51, exitCode) => ({
357942
358305
  ok: false
357943
358306
  });
357944
358307
  async function prepareRun(options, deps) {
358308
+ const environmentId = options.envId?.trim();
358309
+ if (environmentId !== undefined && options.envFile !== undefined) {
358310
+ return refused(interactiveRunnerMessages.envIdWithEnvFile, exitCodes.invalidArgs);
358311
+ }
358312
+ if (environmentId === "") {
358313
+ return refused(interactiveRunnerMessages.envIdBlank, exitCodes.invalidArgs);
358314
+ }
357945
358315
  const linesFilePath = options.linesFile === undefined ? undefined : toCollectedPath(deps.cwd, options.linesFile);
357946
358316
  const roots = linesFilePath === undefined || linesFilePath === options.entryPointPath ? [options.entryPointPath] : [options.entryPointPath, linesFilePath];
357947
358317
  const collected = await collectRunFiles2(deps, roots);
@@ -357958,14 +358328,26 @@ async function prepareRun(options, deps) {
357958
358328
  }
357959
358329
  const given = environment3?.ok === true ? environment3.environment : undefined;
357960
358330
  if (options.lines === undefined) {
357961
- return options.linesFile === undefined ? { environment: given, files, ok: true, selection: undefined } : refused(interactiveRunnerMessages.linesFileWithoutLines, exitCodes.invalidArgs);
358331
+ return options.linesFile === undefined ? {
358332
+ environment: given,
358333
+ environmentId,
358334
+ files,
358335
+ ok: true,
358336
+ selection: undefined
358337
+ } : refused(interactiveRunnerMessages.linesFileWithoutLines, exitCodes.invalidArgs);
357962
358338
  }
357963
358339
  const path9 = linesFilePath ?? options.entryPointPath;
357964
358340
  if (!Object.hasOwn(files, path9)) {
357965
358341
  return refused(interactiveRunnerMessages.fileNotCollected(path9), exitCodes.invalidArgs);
357966
358342
  }
357967
358343
  const built = buildRunSelection({ lines: options.lines, path: path9 });
357968
- return built.ok ? { environment: given, files, ok: true, selection: built.selection } : refused(built.error, exitCodes.invalidArgs);
358344
+ return built.ok ? {
358345
+ environment: given,
358346
+ environmentId,
358347
+ files,
358348
+ ok: true,
358349
+ selection: built.selection
358350
+ } : refused(built.error, exitCodes.invalidArgs);
357969
358351
  }
357970
358352
  async function readEnvironment(envFile, deps) {
357971
358353
  if (envFile === undefined)
@@ -358046,6 +358428,7 @@ async function sendRunFlowRequest(ctx, options, delta) {
358046
358428
  files: delta.files,
358047
358429
  id: options.resolved.runnerId,
358048
358430
  ...options.environment === undefined ? {} : { env: options.environment },
358431
+ ...options.environmentId === undefined ? {} : { environmentId: options.environmentId },
358049
358432
  ...options.selection === undefined ? {} : { selection: options.selection },
358050
358433
  ...delta.unchangedFiles === undefined ? {} : { unchangedFiles: delta.unchangedFiles }
358051
358434
  }, runnerCallOptions);
@@ -358132,6 +358515,7 @@ async function handleRunnerRun(ctx, options, deps) {
358132
358515
  const prepared = await prepareRun({
358133
358516
  entryPointPath,
358134
358517
  envFile: options.envFile,
358518
+ envId: options.envId,
358135
358519
  lines: options.lines,
358136
358520
  linesFile: options.linesFile
358137
358521
  }, deps);
@@ -358153,6 +358537,7 @@ async function handleRunnerRun(ctx, options, deps) {
358153
358537
  const submitted = await submitRun(ctx, {
358154
358538
  entryPointPath,
358155
358539
  environment: prepared.environment,
358540
+ environmentId: prepared.environmentId,
358156
358541
  files: prepared.files,
358157
358542
  resolved,
358158
358543
  selection: prepared.selection
@@ -358191,11 +358576,13 @@ Examples:
358191
358576
  $ qawolf runner run flows/checkout.flow.ts --follow
358192
358577
  $ qawolf runner run flows/checkout.flow.ts --follow --logs
358193
358578
  $ qawolf runner run flows/checkout.flow.ts --lines 12-40
358194
- $ qawolf runner run flows/checkout.flow.ts --lines 4-9 --lines-file pages/login.ts`;
358579
+ $ qawolf runner run flows/checkout.flow.ts --lines 4-9 --lines-file pages/login.ts
358580
+ $ qawolf runner run flows/checkout.flow.ts --env-id staging`;
358195
358581
  function registerRunCommand(runner, signals) {
358196
- declareCommandKind(runner.command("run <flowFile>"), "write").description("Run a flow on an interactive runner, shipping the flow and what it imports").option("--follow", "Report the run's status until it settles: in progress, then passed or failed", false).option("--logs", "Stream every log line the run produces while following. Implies --follow", false).option("--run-events", "Stream the run's progress events as JSON lines while following. Implies --follow", false).option("--recorder-events", "Stream the browser actions the runner records as JSON lines while following, from an anchor taken just before submission: the recorder is runner-wide, not run-scoped. Implies --follow", false).option("--env-file <path>", "Dotenv file whose variables the run is given. Not --env, which on qawolf flows names a QA Wolf environment").option("--lines <start-end>", "Run only these 1-indexed lines against the browser as it stands, instead of the whole flow from a fresh one").option("--lines-file <path>", "File the --lines range lives in. Defaults to <flowFile>; pass it only when the lines are in another file, such as a page object").option("--runner <id>", runnerFlagDescription).option("--timeout <seconds>", "Give up following after this long. Following keeps the runner alive, so a run that never settles would otherwise bill until the terminal closed", String(defaultFollowTimeoutSeconds)).addHelpText("after", runExamples2).action((flowFile, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRun(ctx, {
358582
+ declareCommandKind(runner.command("run <flowFile>"), "write").description("Run a flow on an interactive runner, shipping the flow and what it imports").option("--follow", "Report the run's status until it settles: in progress, then passed or failed", false).option("--logs", "Stream every log line the run produces while following. Implies --follow", false).option("--run-events", "Stream the run's progress events as JSON lines while following. Implies --follow", false).option("--recorder-events", "Stream the browser actions the runner records as JSON lines while following, from an anchor taken just before submission: the recorder is runner-wide, not run-scoped. Implies --follow", false).option("--env-id <env>", "QA Wolf environment whose variables the run is given, by id or alias. QA Wolf reads and decrypts them itself, so they never leave the server and no size limit applies to them").option("--env-file <path>", "Dotenv file on this machine whose variables the run is given. Pass this or --env-id, not both").option("--lines <start-end>", "Run only these 1-indexed lines against the browser as it stands, instead of the whole flow from a fresh one").option("--lines-file <path>", "File the --lines range lives in. Defaults to <flowFile>; pass it only when the lines are in another file, such as a page object").option("--runner <id>", runnerFlagDescription).option("--timeout <seconds>", "Give up following after this long. Following keeps the runner alive, so a run that never settles would otherwise bill until the terminal closed", String(defaultFollowTimeoutSeconds)).addHelpText("after", runExamples2).action((flowFile, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRun(ctx, {
358197
358583
  entryPoint: flowFile,
358198
358584
  envFile: opts.envFile,
358585
+ envId: opts.envId,
358199
358586
  follow: opts.follow,
358200
358587
  lines: opts.lines,
358201
358588
  linesFile: opts.linesFile,
@@ -358216,6 +358603,8 @@ function registerRunnerCommand(program2, signals) {
358216
358603
  registerRunnerInteractCommands(runner, signals);
358217
358604
  registerRunnerInspectCommands(runner, signals);
358218
358605
  registerRunnerImportPackageCommand(runner, signals);
358606
+ registerRunnerHighlightSelectorCommand(runner, signals);
358607
+ registerRunnerPromoteSnapshotCommand(runner, signals);
358219
358608
  }
358220
358609
 
358221
358610
  // src/commands/program.ts
@@ -358254,4 +358643,4 @@ createProgram({ signals }).parseAsync().catch(() => {
358254
358643
  process.exitCode = 1;
358255
358644
  }).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
358256
358645
 
358257
- //# debugId=A9041D278FF748EF64756E2164756E21
358646
+ //# debugId=408E9965E881500264756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qawolf/cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
5
5
  "keywords": [
6
6
  "automation",
@@ -60,7 +60,7 @@
60
60
  "@clack/prompts": "1.5.1",
61
61
  "@napi-rs/keyring": "1.3.0",
62
62
  "@oxc-node/core": "0.1.0",
63
- "@qawolf/api-contracts": "0.32.0",
63
+ "@qawolf/api-contracts": "0.34.0",
64
64
  "@qawolf/emails": "1.1.1",
65
65
  "@qawolf/flow-targets": "1.0.0",
66
66
  "@qawolf/flows": "0.1.4",
@@ -145,12 +145,14 @@ current branch.
145
145
  | `qawolf runner act` | write | Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin |
146
146
  | `qawolf runner events` | read | Print a runner's journal, one entry per line. QA Wolf writes console, recorder, run-events, run-logs, run-status |
147
147
  | `qawolf runner exec` | write | Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin |
148
+ | `qawolf runner highlight-selector` | write | Highlight what a selector matches on a runner's live page, so the next screenshot shows it. Omit the selector to clear the highlight |
148
149
  | `qawolf runner import-package` | write | Install a package into a runner's live run, so a snippet or a selection can import it |
149
150
  | `qawolf runner inspect element-html` | read | Print the HTML of the first element a selector matches |
150
151
  | `qawolf runner inspect page-html` | read | Print the page's HTML, simplified for a model to read |
151
152
  | `qawolf runner inspect variable` | read | Print a top-level variable's value from the running workflow |
152
153
  | `qawolf runner keepalive` | read | Reset a runner's inactivity clock, for a caller that pauses between actions |
153
154
  | `qawolf runner launch` | write | Launch an interactive runner and make it this directory's default |
155
+ | `qawolf runner promote-snapshot` | write | Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it |
154
156
  | `qawolf runner run` | write | Run a flow on an interactive runner, shipping the flow and what it imports |
155
157
  | `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file |
156
158
  | `qawolf runner stop-run` | write | Stop what a runner is currently executing, leaving the runner up |
@@ -159,6 +159,53 @@ Use `inspect` before reaching for `exec`. Reading a value through a snippet
159
159
  means printing it and then fishing it back out of the `console` stream, which is
160
160
  two calls and a marker; `inspect variable` is one call and the value.
161
161
 
162
+ ## Checking a selector: `highlight-selector`
163
+
164
+ `inspect element-html` tells you what a selector matched. `highlight-selector`
165
+ shows you _where_ it is, by drawing on the page itself:
166
+
167
+ ```sh
168
+ qawolf runner highlight-selector "text=Sign in"
169
+ qawolf runner screenshot # the highlight is in this
170
+ qawolf runner highlight-selector # omit the selector to clear
171
+ ```
172
+
173
+ The highlight stays until it is replaced or cleared, which is the point: you
174
+ cannot see the runner's screen, so the only way to read the result is the next
175
+ screenshot.
176
+
177
+ Three answers are worth telling apart. A selector that matched prints how many
178
+ elements it hit and exits `0`. A selector the page read fine but that matched
179
+ nothing also exits `0`, because the call did what was asked and the count is the
180
+ answer; the message says the syntax was fine so you look at the page, not the
181
+ locator. A selector the page could not read at all exits `2`, because that one
182
+ is yours to correct and retrying will not change it.
183
+
184
+ `runner-cannot-highlight-selectors` exits `2` and means the runner has no
185
+ browser to draw on. `no-answer` exits `4`: a highlight runs inside the page, so
186
+ a page that is gone or mid-navigation does not answer at all rather than
187
+ answering slowly.
188
+
189
+ ## Accepting a new baseline: `promote-snapshot`
190
+
191
+ When a run's image diff fails and the new screenshot is the one you want, this
192
+ replaces the baseline on the runner that produced it:
193
+
194
+ ```sh
195
+ qawolf runner events run-events --tail 20 | jq 'select(.type == "image-diff-artifact")'
196
+ qawolf runner promote-snapshot --screenshot checkout-1-actual.png --baseline checkout-1.png
197
+ ```
198
+
199
+ Both paths are the ones the diff reported, and both are named rather than
200
+ positional, because two paths with one unlabelled is easy to get backwards and
201
+ swapping them promotes the wrong image. They are paths inside the run's own
202
+ screenshot storage, not files on your machine.
203
+
204
+ `snapshot-not-found` exits `2` and means the run wrote no screenshot at that
205
+ path, which nearly always means the paths did not come from a diff this run
206
+ produced. Nothing is changed, so correcting the path and repeating is safe.
207
+ Promoting twice is also safe, so an unreachable runner is worth retrying.
208
+
162
209
  ## Installing a package mid-session
163
210
 
164
211
  `qawolf runner import-package <name>` installs a package into the runner's live
@@ -255,14 +302,29 @@ runner is addressed, naming the path.
255
302
 
256
303
  ### Giving the run environment variables
257
304
 
305
+ There are two ways, and a run takes one of them. `--env-id` names a QA Wolf
306
+ environment by id or alias, the same reference `qawolf flows` takes as `--env`.
258
307
  `--env-file .env` gives the run the variables in a dotenv file, in the format
259
- `qawolf flows pull` writes. It is `--env-file` and not `--env`, which on
260
- `qawolf flows` means a QA Wolf environment by id or slug.
308
+ `qawolf flows pull` writes.
309
+
310
+ ```sh
311
+ qawolf runner run flows/checkout.flow.ts --env-id staging
312
+ qawolf runner run flows/checkout.flow.ts --env-file .env
313
+ ```
314
+
315
+ **Prefer `--env-id`.** QA Wolf reads and decrypts the environment itself, so the
316
+ values never leave the server, nothing has to be pulled to disk first, and no
317
+ size limit applies to them. It is the only way to run a flow whose environment
318
+ holds something large, such as a session cookie.
319
+
320
+ A run that sends its own variables with `--env-file` may carry at most 200 of
321
+ them, each value at most 16 KiB. Names follow what a shell accepts, and
322
+ `QAWOLF_TEAM_ID` is reserved because QA Wolf sets it from the key you
323
+ authenticated with. All of that is refused before a runner is addressed, naming
324
+ the variable at fault.
261
325
 
262
- A run may carry at most 100 variables, each value at most 8 KiB. Names follow
263
- what a shell accepts, and `QAWOLF_TEAM_ID` is reserved because QA Wolf sets it
264
- from the key you authenticated with. All of that is refused before a runner is
265
- addressed, naming the variable at fault.
326
+ Passing both flags is refused. They each give the run its whole environment, so
327
+ there is no order in which they would combine.
266
328
 
267
329
  If the runner had no browser, one is started before your lines run, and the
268
330
  command says so on stderr. Those lines then ran against a fresh page rather than