@yoqa/cli 0.3.27 → 0.3.29

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/main.js +648 -24
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @yoqa/cli
2
2
 
3
- CLI for [Yoqa](https://yoqa.mintlify.app/docs/overview) — drive a local runner over HTTP from your terminal or CI.
3
+ CLI for [Yoqa](https://yoqa.arisn.dev/docs/overview) — drive a local runner over HTTP from your terminal or CI.
4
4
 
5
5
  ```bash
6
6
  npm install -g @yoqa/cli
@@ -18,4 +18,4 @@ yoqa serve
18
18
 
19
19
  Disable auto-start with `YOQA_NO_AUTOSTART=1`. Override the URL with `YOQA_RUNNER_HOST` / `YOQA_RUNNER_PORT` / `YOQA_RUNNER_URL`.
20
20
 
21
- See [CLI docs](https://yoqa.mintlify.app/docs/cli) and [GitHub Actions](https://yoqa.mintlify.app/docs/github-actions).
21
+ See [CLI docs](https://yoqa.arisn.dev/docs/cli) and [GitHub Actions](https://yoqa.arisn.dev/docs/github-actions).
package/dist/main.js CHANGED
@@ -2122,7 +2122,7 @@ var require_commander = __commonJS((exports) => {
2122
2122
  });
2123
2123
 
2124
2124
  // src/program.ts
2125
- import { readFile as readFile2 } from "node:fs/promises";
2125
+ import { readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
2126
2126
 
2127
2127
  // ../../node_modules/zod/v3/external.js
2128
2128
  var exports_external = {};
@@ -6105,6 +6105,7 @@ var healthResponseSchema = exports_external.object({
6105
6105
  uptimeMs: exports_external.number().nonnegative()
6106
6106
  });
6107
6107
  var devicePlatformSchema = exports_external.union([exports_external.literal("ios"), exports_external.literal("android")]);
6108
+ var laneNameSchema = exports_external.union([exports_external.literal("appium"), exports_external.literal("direct")]);
6108
6109
  var deviceKindSchema = exports_external.union([
6109
6110
  exports_external.literal("physical"),
6110
6111
  exports_external.literal("simulator"),
@@ -6263,6 +6264,7 @@ var catalogAppSchema = exports_external.object({
6263
6264
  name: exports_external.string().min(1),
6264
6265
  prefix: exports_external.string().min(1),
6265
6266
  context: exports_external.string(),
6267
+ knowledge: exports_external.string(),
6266
6268
  iosBundleId: exports_external.string(),
6267
6269
  iosAppStoreId: exports_external.string(),
6268
6270
  androidApplicationId: exports_external.string(),
@@ -6278,6 +6280,7 @@ var updateAppRequestSchema = exports_external.object({
6278
6280
  name: exports_external.string().min(1).optional(),
6279
6281
  prefix: exports_external.string().min(1).optional(),
6280
6282
  context: exports_external.string().optional(),
6283
+ knowledge: exports_external.string().optional(),
6281
6284
  iosBundleId: exports_external.string().optional(),
6282
6285
  iosAppStoreId: exports_external.string().optional(),
6283
6286
  androidApplicationId: exports_external.string().optional(),
@@ -6591,7 +6594,8 @@ var probeProviderResponseSchema = exports_external.object({
6591
6594
  var providerModelSchema = exports_external.object({
6592
6595
  id: exports_external.string().min(1),
6593
6596
  name: exports_external.string().min(1),
6594
- provider: exports_external.string().min(1).optional()
6597
+ provider: exports_external.string().min(1).optional(),
6598
+ vision: exports_external.boolean().optional()
6595
6599
  });
6596
6600
  var listProviderModelsResponseSchema = exports_external.object({
6597
6601
  models: exports_external.array(providerModelSchema),
@@ -6620,6 +6624,7 @@ var runExecutionModeSchema = exports_external.union([
6620
6624
  exports_external.literal("script"),
6621
6625
  exports_external.literal("agent")
6622
6626
  ]);
6627
+ var runScreenModeSchema = exports_external.union([exports_external.literal("vision"), exports_external.literal("tree")]);
6623
6628
  var createRunRequestSchema = exports_external.object({
6624
6629
  appId: exports_external.string().min(1),
6625
6630
  caseIds: exports_external.array(exports_external.string().min(1)).min(1),
@@ -6627,7 +6632,18 @@ var createRunRequestSchema = exports_external.object({
6627
6632
  platform: devicePlatformSchema,
6628
6633
  buildId: exports_external.string().min(1).optional(),
6629
6634
  buildPath: exports_external.string().min(1).optional(),
6630
- executionMode: runExecutionModeSchema.optional()
6635
+ executionMode: runExecutionModeSchema.optional(),
6636
+ screenMode: runScreenModeSchema.optional(),
6637
+ lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional()
6638
+ });
6639
+ var stepPhasesSchema = exports_external.object({
6640
+ captureMs: exports_external.number().nonnegative(),
6641
+ screenMs: exports_external.number().nonnegative(),
6642
+ prepareMs: exports_external.number().nonnegative(),
6643
+ decideMs: exports_external.number().nonnegative(),
6644
+ actionMs: exports_external.number().nonnegative(),
6645
+ settleMs: exports_external.number().nonnegative(),
6646
+ decideRetries: exports_external.number().int().nonnegative()
6631
6647
  });
6632
6648
  var runStepSchema = exports_external.object({
6633
6649
  id: exports_external.string().min(1),
@@ -6637,6 +6653,7 @@ var runStepSchema = exports_external.object({
6637
6653
  screenshotUri: exports_external.string().nullable(),
6638
6654
  ok: exports_external.boolean(),
6639
6655
  latencyMs: exports_external.number().nonnegative(),
6656
+ phases: stepPhasesSchema.nullable().optional(),
6640
6657
  detail: exports_external.string().nullable(),
6641
6658
  command: exports_external.string().nullable().optional(),
6642
6659
  createdAt: exports_external.number().int().nonnegative()
@@ -6661,6 +6678,9 @@ var runSchema = exports_external.object({
6661
6678
  buildId: exports_external.string().nullable(),
6662
6679
  status: runStatusSchema,
6663
6680
  executionMode: runExecutionModeSchema,
6681
+ screenMode: runScreenModeSchema.optional(),
6682
+ lane: laneNameSchema.optional(),
6683
+ laneWarning: exports_external.string().min(1).optional(),
6664
6684
  error: exports_external.string().nullable(),
6665
6685
  createdAt: exports_external.number().int().nonnegative(),
6666
6686
  startedAt: exports_external.number().int().nonnegative().nullable(),
@@ -6678,7 +6698,8 @@ var connectDeviceRequestSchema = exports_external.object({
6678
6698
  deviceId: exports_external.string().min(1),
6679
6699
  platform: devicePlatformSchema,
6680
6700
  bundleId: exports_external.string().min(1).optional(),
6681
- appPackage: exports_external.string().min(1).optional()
6701
+ appPackage: exports_external.string().min(1).optional(),
6702
+ lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional()
6682
6703
  });
6683
6704
  var activeDeviceResponseSchema = exports_external.object({
6684
6705
  deviceId: exports_external.string().min(1),
@@ -6687,7 +6708,9 @@ var activeDeviceResponseSchema = exports_external.object({
6687
6708
  mjpegPort: exports_external.number().int().positive().optional(),
6688
6709
  streamReady: exports_external.boolean().optional(),
6689
6710
  streamUrl: exports_external.string().min(1).optional(),
6690
- heldByRun: exports_external.boolean().optional()
6711
+ heldByRun: exports_external.boolean().optional(),
6712
+ lane: laneNameSchema.optional(),
6713
+ laneWarning: exports_external.string().min(1).optional()
6691
6714
  });
6692
6715
  var controlPointerMessageSchema = exports_external.object({
6693
6716
  type: exports_external.literal("pointer"),
@@ -6746,7 +6769,24 @@ var actionRequestSchema = exports_external.object({
6746
6769
  appId: exports_external.string().optional(),
6747
6770
  url: exports_external.string().optional(),
6748
6771
  alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
6749
- seconds: exports_external.number().optional()
6772
+ seconds: exports_external.number().optional(),
6773
+ screenshot: exports_external.boolean().optional(),
6774
+ settleMs: exports_external.number().int().min(0).max(1e4).optional(),
6775
+ fullImage: exports_external.boolean().optional(),
6776
+ imageScale: exports_external.number().min(0.01).max(1).optional()
6777
+ });
6778
+ var actionResultScreenshotSchema = exports_external.object({
6779
+ path: exports_external.string().min(1),
6780
+ rawPath: exports_external.string().min(1).optional(),
6781
+ annotatedPath: exports_external.string().min(1).optional(),
6782
+ settled: exports_external.boolean(),
6783
+ waitedMs: exports_external.number(),
6784
+ changed: exports_external.boolean().nullable()
6785
+ });
6786
+ var actionPhasesSchema = exports_external.object({
6787
+ captureMs: exports_external.number().nonnegative(),
6788
+ actionMs: exports_external.number().nonnegative(),
6789
+ settleMs: exports_external.number().nonnegative()
6750
6790
  });
6751
6791
  var actionResponseSchema = exports_external.object({
6752
6792
  ok: exports_external.literal(true),
@@ -6754,7 +6794,24 @@ var actionResponseSchema = exports_external.object({
6754
6794
  resolved: exports_external.object({
6755
6795
  x: exports_external.number().optional(),
6756
6796
  y: exports_external.number().optional()
6757
- }).optional()
6797
+ }).optional(),
6798
+ screenshot: actionResultScreenshotSchema.optional(),
6799
+ phases: actionPhasesSchema.optional()
6800
+ });
6801
+ var actionBatchRequestSchema = exports_external.object({
6802
+ steps: exports_external.array(actionRequestSchema).min(1),
6803
+ settleMs: exports_external.number().int().min(0).max(1e4).optional(),
6804
+ fullImage: exports_external.boolean().optional(),
6805
+ imageScale: exports_external.number().min(0.01).max(1).optional()
6806
+ });
6807
+ var actionBatchResponseSchema = exports_external.object({
6808
+ ok: exports_external.boolean(),
6809
+ completed: exports_external.number().int().nonnegative(),
6810
+ total: exports_external.number().int().nonnegative(),
6811
+ failedIndex: exports_external.number().int().nonnegative().optional(),
6812
+ error: exports_external.string().optional(),
6813
+ screenshot: actionResultScreenshotSchema.optional(),
6814
+ steps: exports_external.array(actionResponseSchema)
6758
6815
  });
6759
6816
  var yoqaStatusResponseSchema = exports_external.object({
6760
6817
  runner: exports_external.object({
@@ -7309,10 +7366,28 @@ async function runYoqaShellScript(client, text, options = {}) {
7309
7366
  }
7310
7367
 
7311
7368
  // ../runner-client/src/run-report.ts
7312
- function actionSummary(action) {
7313
- if (!action || typeof action !== "object")
7314
- return "Step";
7315
- const record = action;
7369
+ function readStepCycle(action) {
7370
+ if (!action || typeof action !== "object" || !("cycle" in action))
7371
+ return null;
7372
+ const cycle = action.cycle;
7373
+ if (!cycle || typeof cycle !== "object")
7374
+ return null;
7375
+ const record = cycle;
7376
+ const asRecord = (value) => value && typeof value === "object" ? value : null;
7377
+ const verify = asRecord(record.verify);
7378
+ if (!verify || typeof verify.type !== "string" || !verify.type)
7379
+ return null;
7380
+ return {
7381
+ action: asRecord(record.action),
7382
+ decision: asRecord(record.decision),
7383
+ verify: {
7384
+ type: verify.type,
7385
+ reason: typeof verify.reason === "string" ? verify.reason : null,
7386
+ thoughts: typeof verify.thoughts === "string" ? verify.thoughts : null
7387
+ }
7388
+ };
7389
+ }
7390
+ function summarizeActionRecord(record) {
7316
7391
  const type = typeof record.type === "string" ? record.type : "step";
7317
7392
  if (type === "tap") {
7318
7393
  if (typeof record.label === "string" && record.label.trim())
@@ -7382,6 +7457,16 @@ function actionSummary(action) {
7382
7457
  }
7383
7458
  return type.charAt(0).toUpperCase() + type.slice(1);
7384
7459
  }
7460
+ function actionSummary(action) {
7461
+ if (!action || typeof action !== "object")
7462
+ return "Step";
7463
+ const cycle = readStepCycle(action);
7464
+ if (cycle?.action)
7465
+ return summarizeActionRecord(cycle.action);
7466
+ if (cycle?.decision)
7467
+ return summarizeActionRecord(cycle.decision);
7468
+ return summarizeActionRecord(action);
7469
+ }
7385
7470
  function optionalTrimmedString(value) {
7386
7471
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
7387
7472
  }
@@ -7489,6 +7574,20 @@ function toReportStatus(status) {
7489
7574
  return status;
7490
7575
  return null;
7491
7576
  }
7577
+ function formatStepPhases(phases) {
7578
+ const parts = [
7579
+ `capture ${Math.round(phases.captureMs)}ms`,
7580
+ `screen ${Math.round(phases.screenMs)}ms`,
7581
+ `image ${Math.round(phases.prepareMs)}ms`,
7582
+ `decide ${Math.round(phases.decideMs)}ms`,
7583
+ `action ${Math.round(phases.actionMs)}ms`,
7584
+ `settle ${Math.round(phases.settleMs)}ms`
7585
+ ];
7586
+ if (phases.decideRetries > 0) {
7587
+ parts.push(`retries ${phases.decideRetries}`);
7588
+ }
7589
+ return parts.join(" · ");
7590
+ }
7492
7591
  function mapCatalogStep(step, screenshotsByStepId) {
7493
7592
  const { reason, thoughts } = stepReasoning(step);
7494
7593
  return {
@@ -7497,6 +7596,7 @@ function mapCatalogStep(step, screenshotsByStepId) {
7497
7596
  summary: actionSummary(step.action),
7498
7597
  ok: step.ok,
7499
7598
  latencyMs: step.latencyMs,
7599
+ phases: step.phases ?? null,
7500
7600
  detail: step.detail,
7501
7601
  reason,
7502
7602
  thoughts,
@@ -7530,6 +7630,9 @@ function buildRunReportFromCatalogRun(run, meta = {}, screenshotsByStepId = {})
7530
7630
  deviceLabel: meta.deviceLabel ?? null,
7531
7631
  platform: run.platform,
7532
7632
  executionMode: run.executionMode,
7633
+ screenMode: run.screenMode ?? null,
7634
+ lane: run.lane ?? null,
7635
+ laneWarning: run.laneWarning ?? null,
7533
7636
  error: run.error,
7534
7637
  createdAt: run.createdAt,
7535
7638
  startedAt: run.startedAt,
@@ -7577,6 +7680,11 @@ function pngDataUri(base64) {
7577
7680
  }
7578
7681
  function formatRunReportHtml(doc) {
7579
7682
  const accent = statusAccent(doc.status);
7683
+ const screenRows = [
7684
+ ...doc.screenMode ? [["Screen", doc.screenMode]] : [],
7685
+ ...doc.lane ? [["Lane", doc.lane]] : [],
7686
+ ...doc.laneWarning ? [["Lane warning", doc.laneWarning]] : []
7687
+ ];
7580
7688
  const metaRows = [
7581
7689
  ["Status", accent.label],
7582
7690
  ["Source", doc.source === "catalog" ? "Catalog run" : "Manual Inspector"],
@@ -7584,6 +7692,7 @@ function formatRunReportHtml(doc) {
7584
7692
  ["Device", doc.deviceLabel ?? "—"],
7585
7693
  ["Platform", doc.platform ?? "—"],
7586
7694
  ["Mode", doc.executionMode ?? "—"],
7695
+ ...screenRows,
7587
7696
  ["Started", formatWhen(doc.startedAt ?? doc.createdAt)],
7588
7697
  ["Finished", formatWhen(doc.finishedAt)],
7589
7698
  ["Duration", formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt)],
@@ -7599,6 +7708,7 @@ function formatRunReportHtml(doc) {
7599
7708
  const detailBlock = step.detail && step.detail !== step.reason ? `<p class="detail">${escapeHtml(step.detail)}</p>` : "";
7600
7709
  const shot = step.screenshotBase64 ? `<img class="shot" alt="Step ${step.index} screenshot" src="${pngDataUri(step.screenshotBase64)}" />` : `<p class="muted">No screenshot</p>`;
7601
7710
  const latency = step.latencyMs != null ? `<span class="muted">${step.latencyMs}ms</span>` : "";
7711
+ const phases = step.phases ? `<p class="muted phases">Phases: ${escapeHtml(formatStepPhases(step.phases))}</p>` : "";
7602
7712
  const commandBlock = step.command ? `<pre class="command"><code>${escapeHtml(step.command)}</code></pre>` : "";
7603
7713
  return `
7604
7714
  <article class="step ${step.ok ? "ok" : "fail"}">
@@ -7610,6 +7720,7 @@ function formatRunReportHtml(doc) {
7610
7720
  </div>
7611
7721
  </header>
7612
7722
  ${commandBlock}
7723
+ ${phases}
7613
7724
  ${reasonBlock}
7614
7725
  ${detailBlock}
7615
7726
  ${thoughtsBlock}
@@ -7707,6 +7818,9 @@ function formatRunReportMarkdown(doc) {
7707
7818
  `| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
7708
7819
  `| Platform | ${escapeMd(doc.platform ?? "—")} |`,
7709
7820
  `| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
7821
+ ...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
7822
+ ...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
7823
+ ...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
7710
7824
  `| Started | ${escapeMd(formatWhen(doc.startedAt ?? doc.createdAt))} |`,
7711
7825
  `| Finished | ${escapeMd(formatWhen(doc.finishedAt))} |`,
7712
7826
  `| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
@@ -7735,6 +7849,8 @@ function formatRunReportMarkdown(doc) {
7735
7849
  lines.push(`- Command: \`${step.command.replace(/`/g, "'")}\``);
7736
7850
  if (step.latencyMs != null)
7737
7851
  lines.push(`- Latency: ${step.latencyMs}ms`);
7852
+ if (step.phases)
7853
+ lines.push(`- Phases: ${formatStepPhases(step.phases)}`);
7738
7854
  if (step.reason)
7739
7855
  lines.push(`- Reason: ${step.reason}`);
7740
7856
  if (step.detail && step.detail !== step.reason)
@@ -7771,6 +7887,9 @@ function formatRunReportGithubSummary(doc) {
7771
7887
  `| Device | ${escapeMd(doc.deviceLabel ?? "—")} |`,
7772
7888
  `| Platform | ${escapeMd(doc.platform ?? "—")} |`,
7773
7889
  `| Mode | ${escapeMd(doc.executionMode ?? "—")} |`,
7890
+ ...doc.screenMode ? [`| Screen | ${escapeMd(doc.screenMode)} |`] : [],
7891
+ ...doc.lane ? [`| Lane | ${escapeMd(doc.lane)} |`] : [],
7892
+ ...doc.laneWarning ? [`| Lane warning | ${escapeMd(doc.laneWarning)} |`] : [],
7774
7893
  `| Duration | ${escapeMd(formatDuration(doc.startedAt ?? doc.createdAt, doc.finishedAt))} |`,
7775
7894
  `| Run ID | \`${doc.id}\` |`,
7776
7895
  ""
@@ -8226,6 +8345,15 @@ class RunnerClient {
8226
8345
  }, "Action failed");
8227
8346
  return actionResponseSchema.parse(json);
8228
8347
  }
8348
+ async performActionBatch(request) {
8349
+ const body = actionBatchRequestSchema.parse(request);
8350
+ const json = await this.requestJson("/action/batch", {
8351
+ method: "POST",
8352
+ headers: { "Content-Type": "application/json" },
8353
+ body: JSON.stringify(body)
8354
+ }, "Action batch failed");
8355
+ return actionBatchResponseSchema.parse(json);
8356
+ }
8229
8357
  async getStatus() {
8230
8358
  const json = await this.requestJson("/status", undefined, "Status failed");
8231
8359
  return yoqaStatusResponseSchema.parse(json);
@@ -8274,10 +8402,369 @@ var {
8274
8402
  Option,
8275
8403
  Help
8276
8404
  } = import__.default;
8405
+
8406
+ // ../../services/runner/src/domains/benchmark/accuracy.ts
8407
+ function pointInElement(element, x, y) {
8408
+ return x >= element.x && x <= element.x + element.width && y >= element.y && y <= element.y + element.height;
8409
+ }
8410
+ function tapHitsElement(elements, x, y, expect) {
8411
+ const intended = expect?.id ? elements.filter((el) => el.id === expect.id) : expect?.label ? elements.filter((el) => el.label === expect.label) : elements;
8412
+ return intended.some((el) => pointInElement(el, x, y));
8413
+ }
8414
+
8415
+ // ../../services/runner/src/domains/benchmark/drivers.ts
8416
+ function yoqaBenchmarkDriver(client, options) {
8417
+ return {
8418
+ name: "yoqa",
8419
+ connect: async () => {
8420
+ await client.connectDevice({
8421
+ deviceId: options.deviceId,
8422
+ platform: options.platform,
8423
+ lane: options.lane
8424
+ });
8425
+ },
8426
+ screen: async () => {
8427
+ await client.getScreen();
8428
+ },
8429
+ tapToResult: async (x, y, expect) => {
8430
+ const result = await client.performAction({ kind: "tap", x, y, screenshot: true });
8431
+ const tree = await client.getScreen().catch(() => ({ elements: [] }));
8432
+ const hit = tapHitsElement(tree.elements ?? [], x, y, expect);
8433
+ return {
8434
+ hit,
8435
+ phases: {
8436
+ capture: result.phases?.captureMs ?? 0,
8437
+ action: result.phases?.actionMs ?? 0,
8438
+ settle: result.phases?.settleMs ?? result.screenshot?.waitedMs ?? 0
8439
+ }
8440
+ };
8441
+ },
8442
+ runCase: client.createRun && client.waitForRun && options.appId ? async (caseId) => {
8443
+ const created = await client.createRun?.({
8444
+ appId: options.appId ?? "",
8445
+ caseIds: [caseId],
8446
+ deviceId: options.deviceId,
8447
+ platform: options.platform,
8448
+ executionMode: "agent",
8449
+ screenMode: options.screenMode,
8450
+ lane: options.lane
8451
+ });
8452
+ if (!created)
8453
+ return { passed: false };
8454
+ const finished = await client.waitForRun?.(created.id);
8455
+ const steps = finished?.tests?.reduce((n, test) => n + (test.steps?.length ?? 0), 0);
8456
+ return { passed: finished?.status === "passed", steps };
8457
+ } : undefined,
8458
+ disconnect: async () => {
8459
+ await client.disconnectDevice();
8460
+ }
8461
+ };
8462
+ }
8463
+
8464
+ // ../../services/runner/src/domains/benchmark/scenario.ts
8465
+ async function runLatencyScenario(driver, options) {
8466
+ const clock = options.clock ?? { now: () => Date.now() };
8467
+ const taps = options.suite?.taps ?? [{ x: options.x ?? 500, y: options.y ?? 500 }];
8468
+ const tapToResult = [];
8469
+ const screenRead = [];
8470
+ const coldStart = [];
8471
+ const tapHits = [];
8472
+ const casePasses = [];
8473
+ const stepCounts = [];
8474
+ const captures = [];
8475
+ const actions = [];
8476
+ const settles = [];
8477
+ const t0 = clock.now();
8478
+ await driver.connect();
8479
+ options.advance?.(50);
8480
+ coldStart.push(clock.now() - t0);
8481
+ const tScreen = clock.now();
8482
+ await driver.screen();
8483
+ options.advance?.(20);
8484
+ screenRead.push(clock.now() - tScreen);
8485
+ for (let i = 0;i < options.repeats; i++) {
8486
+ const tap = taps[i % taps.length];
8487
+ if (!tap)
8488
+ continue;
8489
+ const tTap = clock.now();
8490
+ const result = await driver.tapToResult(tap.x, tap.y, {
8491
+ id: tap.id,
8492
+ label: tap.label
8493
+ });
8494
+ options.advance?.(30);
8495
+ tapToResult.push(clock.now() - tTap);
8496
+ if (result.hit != null)
8497
+ tapHits.push(result.hit);
8498
+ if (result.phases?.capture != null)
8499
+ captures.push(result.phases.capture);
8500
+ if (result.phases?.action != null)
8501
+ actions.push(result.phases.action);
8502
+ if (result.phases?.settle != null)
8503
+ settles.push(result.phases.settle);
8504
+ }
8505
+ if (driver.runCase && options.suite) {
8506
+ const times = options.suite.passRepeats;
8507
+ for (let i = 0;i < times; i++) {
8508
+ for (const item of options.suite.cases) {
8509
+ const outcome = await driver.runCase(item.caseId);
8510
+ const passed = typeof outcome === "boolean" ? outcome : outcome.passed;
8511
+ casePasses.push(passed);
8512
+ if (typeof outcome !== "boolean" && outcome.steps != null) {
8513
+ stepCounts.push(outcome.steps);
8514
+ }
8515
+ }
8516
+ }
8517
+ }
8518
+ await driver.disconnect();
8519
+ const phases = driver.name === "yoqa" && (captures.length || actions.length || settles.length) ? {
8520
+ ...captures.length ? { capture: captures } : {},
8521
+ ...actions.length ? { action: actions } : {},
8522
+ ...settles.length ? { settle: settles } : {}
8523
+ } : undefined;
8524
+ return {
8525
+ tool: driver.name,
8526
+ platform: options.platform ?? "ios",
8527
+ ...options.screenMode ? { screenMode: options.screenMode } : {},
8528
+ tapToResult,
8529
+ screenRead,
8530
+ coldStart,
8531
+ ...tapHits.length ? { tapHits } : {},
8532
+ ...casePasses.length ? { casePasses } : {},
8533
+ ...stepCounts.length ? { stepCounts } : {},
8534
+ ...phases ? { phases } : {}
8535
+ };
8536
+ }
8537
+
8538
+ // ../../services/runner/src/domains/benchmark/suite.ts
8539
+ var BENCHMARK_SUITE_VERSION = 1;
8540
+ var DEFAULT_BENCHMARK_SUITE = {
8541
+ version: BENCHMARK_SUITE_VERSION,
8542
+ repeats: 5,
8543
+ taps: [{ x: 500, y: 500 }],
8544
+ cases: [],
8545
+ passRepeats: 3,
8546
+ arms: ["vision"]
8547
+ };
8548
+ function parseBenchmarkSuite(raw) {
8549
+ if (!raw || typeof raw !== "object") {
8550
+ throw new Error("Benchmark suite must be an object");
8551
+ }
8552
+ const value = raw;
8553
+ const version = Number(value.version);
8554
+ const repeats = Number(value.repeats);
8555
+ const passRepeats = Number(value.passRepeats ?? 3);
8556
+ if (!Number.isInteger(version) || version < 1) {
8557
+ throw new Error("Benchmark suite version must be a positive integer");
8558
+ }
8559
+ if (!Number.isInteger(repeats) || repeats < 1) {
8560
+ throw new Error("Benchmark suite repeats must be a positive integer");
8561
+ }
8562
+ if (!Array.isArray(value.taps) || value.taps.length === 0) {
8563
+ throw new Error("Benchmark suite needs at least one tap");
8564
+ }
8565
+ const taps = value.taps.map((tap, i) => {
8566
+ if (!tap || typeof tap !== "object") {
8567
+ throw new Error(`Benchmark suite tap ${i} is invalid`);
8568
+ }
8569
+ const row = tap;
8570
+ const x = Number(row.x);
8571
+ const y = Number(row.y);
8572
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
8573
+ throw new Error(`Benchmark suite tap ${i} needs x and y`);
8574
+ }
8575
+ return {
8576
+ x,
8577
+ y,
8578
+ ...typeof row.id === "string" ? { id: row.id } : {},
8579
+ ...typeof row.label === "string" ? { label: row.label } : {}
8580
+ };
8581
+ });
8582
+ const cases = Array.isArray(value.cases) ? value.cases.flatMap((item) => {
8583
+ if (!item || typeof item !== "object")
8584
+ return [];
8585
+ const caseId = item.caseId;
8586
+ return typeof caseId === "string" && caseId.length > 0 ? [{ caseId }] : [];
8587
+ }) : [];
8588
+ return {
8589
+ version,
8590
+ repeats,
8591
+ taps,
8592
+ cases,
8593
+ passRepeats: Number.isInteger(passRepeats) ? passRepeats : 3,
8594
+ arms: parseBenchmarkArms(value.arms)
8595
+ };
8596
+ }
8597
+ function parseBenchmarkArms(raw) {
8598
+ if (raw == null)
8599
+ return ["vision"];
8600
+ if (!Array.isArray(raw) || raw.length === 0) {
8601
+ throw new Error("Benchmark suite arms must be a non-empty list of vision | tree");
8602
+ }
8603
+ const arms = [];
8604
+ for (const item of raw) {
8605
+ if (item !== "vision" && item !== "tree") {
8606
+ throw new Error("Benchmark suite arms must be vision or tree");
8607
+ }
8608
+ if (!arms.includes(item))
8609
+ arms.push(item);
8610
+ }
8611
+ return arms;
8612
+ }
8613
+
8614
+ // ../../services/runner/src/domains/benchmark/summarize.ts
8615
+ function percentile(samples, p) {
8616
+ if (samples.length === 0)
8617
+ return 0;
8618
+ const sorted = [...samples].sort((a, b) => a - b);
8619
+ const rank = Math.ceil(p / 100 * sorted.length) - 1;
8620
+ return sorted[Math.max(0, Math.min(sorted.length - 1, rank))] ?? 0;
8621
+ }
8622
+ function summarizeSamples(samples) {
8623
+ return {
8624
+ p50: percentile(samples, 50),
8625
+ p95: percentile(samples, 95),
8626
+ n: samples.length,
8627
+ samples: [...samples]
8628
+ };
8629
+ }
8630
+ function mean(samples) {
8631
+ if (!samples || samples.length === 0)
8632
+ return;
8633
+ return samples.reduce((sum, n) => sum + n, 0) / samples.length;
8634
+ }
8635
+ function summarizeRate(hits) {
8636
+ if (!hits || hits.length === 0)
8637
+ return;
8638
+ const n = hits.length;
8639
+ const count = hits.filter(Boolean).length;
8640
+ return { hits: count, n, rate: count / n };
8641
+ }
8642
+ function summarizeLatency(input) {
8643
+ return {
8644
+ recordedAt: input.recordedAt,
8645
+ repeats: input.repeats,
8646
+ ...input.suiteVersion != null ? { suiteVersion: input.suiteVersion } : {},
8647
+ results: input.runs.map((run) => {
8648
+ const capture = mean(run.phases?.capture);
8649
+ const action = mean(run.phases?.action);
8650
+ const settle = mean(run.phases?.settle);
8651
+ const phases = capture != null || action != null || settle != null ? {
8652
+ ...capture != null ? { capture } : {},
8653
+ ...action != null ? { action } : {},
8654
+ ...settle != null ? { settle } : {}
8655
+ } : undefined;
8656
+ const accuracy = summarizeRate(run.tapHits);
8657
+ const passRate = summarizeRate(run.casePasses);
8658
+ const steps = run.stepCounts?.length ? summarizeSamples(run.stepCounts) : undefined;
8659
+ return {
8660
+ tool: run.tool,
8661
+ platform: run.platform,
8662
+ ...run.screenMode ? { screenMode: run.screenMode } : {},
8663
+ metrics: {
8664
+ tapToResult: summarizeSamples(run.tapToResult),
8665
+ screenRead: summarizeSamples(run.screenRead),
8666
+ coldStart: summarizeSamples(run.coldStart)
8667
+ },
8668
+ ...accuracy ? { accuracy } : {},
8669
+ ...passRate ? { passRate } : {},
8670
+ ...steps ? { steps } : {},
8671
+ ...phases ? { phases } : {}
8672
+ };
8673
+ })
8674
+ };
8675
+ }
8676
+ function formatLatencyTable(report) {
8677
+ const header = [
8678
+ "tool".padEnd(12),
8679
+ "platform".padEnd(10),
8680
+ "metric".padEnd(14),
8681
+ "p50".padStart(8),
8682
+ "p95".padStart(8),
8683
+ "n".padStart(4)
8684
+ ].join(" ");
8685
+ const lines = [header];
8686
+ for (const row of report.results) {
8687
+ const tool = row.screenMode ? `${row.tool}/${row.screenMode}` : row.tool;
8688
+ for (const metric of ["tapToResult", "screenRead", "coldStart"]) {
8689
+ const s = row.metrics[metric];
8690
+ lines.push([
8691
+ tool.padEnd(12),
8692
+ row.platform.padEnd(10),
8693
+ metric.padEnd(14),
8694
+ String(Math.round(s.p50)).padStart(8),
8695
+ String(Math.round(s.p95)).padStart(8),
8696
+ String(s.n).padStart(4)
8697
+ ].join(" "));
8698
+ }
8699
+ if (row.accuracy) {
8700
+ lines.push([
8701
+ tool.padEnd(12),
8702
+ row.platform.padEnd(10),
8703
+ "tapAccuracy".padEnd(14),
8704
+ row.accuracy.rate.toFixed(2).padStart(8),
8705
+ "".padStart(8),
8706
+ String(row.accuracy.n).padStart(4)
8707
+ ].join(" "));
8708
+ }
8709
+ if (row.passRate) {
8710
+ lines.push([
8711
+ tool.padEnd(12),
8712
+ row.platform.padEnd(10),
8713
+ "casePassRate".padEnd(14),
8714
+ row.passRate.rate.toFixed(2).padStart(8),
8715
+ "".padStart(8),
8716
+ String(row.passRate.n).padStart(4)
8717
+ ].join(" "));
8718
+ }
8719
+ if (row.steps) {
8720
+ lines.push([
8721
+ tool.padEnd(12),
8722
+ row.platform.padEnd(10),
8723
+ "stepCount".padEnd(14),
8724
+ String(Math.round(row.steps.p50)).padStart(8),
8725
+ String(Math.round(row.steps.p95)).padStart(8),
8726
+ String(row.steps.n).padStart(4)
8727
+ ].join(" "));
8728
+ }
8729
+ if (row.phases) {
8730
+ const parts = Object.entries(row.phases).map(([k, v]) => `${k}=${Math.round(v)}`);
8731
+ lines.push(` phases ${parts.join(" ")}`);
8732
+ }
8733
+ }
8734
+ return `${lines.join(`
8735
+ `)}
8736
+ `;
8737
+ }
8738
+ function recommendScreenDefault(vision, tree) {
8739
+ if (!vision || !tree) {
8740
+ return {
8741
+ screenMode: "vision",
8742
+ reason: "Need both vision and tree arms before flipping the default"
8743
+ };
8744
+ }
8745
+ if (!vision.passRate || !tree.passRate) {
8746
+ return {
8747
+ screenMode: "vision",
8748
+ reason: "No Case pass-rate samples; keep vision-first"
8749
+ };
8750
+ }
8751
+ const visionAcc = vision.accuracy?.rate ?? 0;
8752
+ const treeAcc = tree.accuracy?.rate ?? 0;
8753
+ if (tree.passRate.rate > vision.passRate.rate && treeAcc >= visionAcc) {
8754
+ return {
8755
+ screenMode: "tree",
8756
+ reason: "Tree arm won Case pass rate without losing tap accuracy"
8757
+ };
8758
+ }
8759
+ return {
8760
+ screenMode: "vision",
8761
+ reason: "Tree arm did not beat vision on pass rate and accuracy; keep vision-first"
8762
+ };
8763
+ }
8277
8764
  // package.json
8278
8765
  var package_default = {
8279
8766
  name: "@yoqa/cli",
8280
- version: "0.3.27",
8767
+ version: "0.3.29",
8281
8768
  description: "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
8282
8769
  license: "UNLICENSED",
8283
8770
  type: "module",
@@ -8295,7 +8782,7 @@ var package_default = {
8295
8782
  url: "git+https://github.com/Aris-ngoy/qa-agent.git",
8296
8783
  directory: "packages/cli"
8297
8784
  },
8298
- homepage: "https://yoqa.mintlify.app/docs/cli",
8785
+ homepage: "https://yoqa.arisn.dev/docs/cli",
8299
8786
  bugs: {
8300
8787
  url: "https://github.com/Aris-ngoy/qa-agent/issues"
8301
8788
  },
@@ -8315,7 +8802,7 @@ var package_default = {
8315
8802
  health: "bun run src/main.ts health"
8316
8803
  },
8317
8804
  dependencies: {
8318
- "@yoqa/runner": "0.3.27"
8805
+ "@yoqa/runner": "0.3.29"
8319
8806
  },
8320
8807
  devDependencies: {
8321
8808
  "@types/node": "^22.15.30",
@@ -9166,23 +9653,30 @@ for (const platform of ["ios", "android"]) {
9166
9653
  }
9167
9654
  });
9168
9655
  }
9169
- devices.command("connect").description("Open an Appium session on a device").argument("<deviceId>", "Device UDID / serial").requiredOption("--platform <platform>", "ios | android").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--bundle-id <id>", "iOS bundle id to launch").option("--app-package <id>", "Android application id to launch").option("--json", "Print raw JSON").action(async (deviceId, options) => {
9656
+ devices.command("connect").description("Open a Device Session on a device").argument("<deviceId>", "Device UDID / serial").requiredOption("--platform <platform>", "ios | android").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--bundle-id <id>", "iOS bundle id to launch").option("--app-package <id>", "Android application id to launch").option("--lane <lane>", "appium | direct | auto (Android / iOS sim auto → Direct)").option("--json", "Print raw JSON").action(async (deviceId, options) => {
9170
9657
  try {
9171
9658
  const platform = options.platform;
9172
9659
  if (platform !== "ios" && platform !== "android") {
9173
9660
  throw new Error("--platform must be ios or android");
9174
9661
  }
9662
+ const lane = options.lane;
9663
+ if (lane != null && lane !== "appium" && lane !== "direct" && lane !== "auto") {
9664
+ throw new Error("--lane must be appium, direct, or auto");
9665
+ }
9175
9666
  const body = await client(options.baseUrl).connectDevice({
9176
9667
  deviceId,
9177
9668
  platform,
9178
9669
  bundleId: options.bundleId,
9179
- appPackage: options.appPackage
9670
+ appPackage: options.appPackage,
9671
+ lane
9180
9672
  });
9181
9673
  if (options.json) {
9182
9674
  console.log(JSON.stringify(body, null, 2));
9183
9675
  return;
9184
9676
  }
9185
- console.log(`connected ${body.platform} ${body.deviceId}`);
9677
+ console.log(`connected ${body.platform} ${body.deviceId} lane ${body.lane ?? "appium"}`);
9678
+ if (body.laneWarning)
9679
+ console.log(`lane warning: ${body.laneWarning}`);
9186
9680
  } catch (error) {
9187
9681
  fail("devices connect", error);
9188
9682
  }
@@ -9199,7 +9693,9 @@ devices.command("active").description("Show the currently connected device").opt
9199
9693
  console.log(JSON.stringify(body, null, 2));
9200
9694
  return;
9201
9695
  }
9202
- console.log(`${body.platform} ${body.deviceId}`);
9696
+ console.log(`${body.platform} ${body.deviceId} lane ${body.lane ?? "appium"}`);
9697
+ if (body.laneWarning)
9698
+ console.log(`lane warning: ${body.laneWarning}`);
9203
9699
  } catch (error) {
9204
9700
  fail("devices active", error);
9205
9701
  }
@@ -9244,7 +9740,19 @@ program2.command("screenshot").description("Capture a screenshot from the active
9244
9740
  });
9245
9741
  var action = program2.command("action").description("Perform an action on the active device");
9246
9742
  function addActionOptions(cmd) {
9247
- return cmd.option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("-d, --description <text>", "Ground target by natural language").option("--label <text>", "Tap/focus by accessibility label (cleaned screen tree)").option("--id <text>", "Tap/focus by resource-id / accessibility id").option("--x <n>", "X in 0–1000", (v) => Number(v)).option("--y <n>", "Y in 0–1000", (v) => Number(v)).option("--x2 <n>", "End X in 0–1000", (v) => Number(v)).option("--y2 <n>", "End Y in 0–1000", (v) => Number(v)).option("--duration <ms>", "Gesture duration ms (long-press for tap)", (v) => Number(v)).option("--double", "Double-tap (tap only)").option("--text <text>", "Text to type").option("--app-id <id>", "Bundle id / application id").option("--url <url>", "URL to open").option("--seconds <n>", "Background seconds", (v) => Number(v));
9743
+ return cmd.option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("-d, --description <text>", "Ground target by natural language").option("--label <text>", "Tap/focus by accessibility label (cleaned screen tree)").option("--id <text>", "Tap/focus by resource-id / accessibility id").option("--x <n>", "X in 0–1000", (v) => Number(v)).option("--y <n>", "Y in 0–1000", (v) => Number(v)).option("--x2 <n>", "End X in 0–1000", (v) => Number(v)).option("--y2 <n>", "End Y in 0–1000", (v) => Number(v)).option("--duration <ms>", "Gesture duration ms (long-press for tap)", (v) => Number(v)).option("--double", "Double-tap (tap only)").option("--text <text>", "Text to type").option("--app-id <id>", "Bundle id / application id").option("--url <url>", "URL to open").option("--seconds <n>", "Background seconds", (v) => Number(v)).option("--screenshot", "Wait for the screen to settle, then return the result screenshot (and a copy marking where the tap or swipe landed)").option("--settle <ms>", "With --screenshot: longest wait for the screen to settle (default 1500)", (v) => Number(v)).option("--full", "With --screenshot: return the raw Result screenshot instead of the Agent image").option("--scale <n>", "With --screenshot: Agent image scale 0.01–1 (default: long edge ~1000 px)", (v) => Number(v));
9744
+ }
9745
+ function printResultScreenshot(shot) {
9746
+ console.log(`screenshot ${shot.path}`);
9747
+ if (shot.rawPath)
9748
+ console.log(`raw ${shot.rawPath}`);
9749
+ if (shot.annotatedPath)
9750
+ console.log(`marked ${shot.annotatedPath}`);
9751
+ console.log(`settled ${shot.settled} (${Math.round(shot.waitedMs)}ms)`);
9752
+ console.log(`changed ${shot.changed === null ? "unknown (screen still animating)" : shot.changed}`);
9753
+ }
9754
+ function printActionPhases(phases) {
9755
+ console.log(`phases capture=${phases.captureMs} action=${phases.actionMs} settle=${phases.settleMs}`);
9248
9756
  }
9249
9757
  for (const kind of [
9250
9758
  "tap",
@@ -9273,7 +9781,11 @@ for (const kind of [
9273
9781
  description: options.description,
9274
9782
  appId: options.appId,
9275
9783
  url: options.url,
9276
- seconds: options.seconds
9784
+ seconds: options.seconds,
9785
+ screenshot: options.screenshot === true ? true : undefined,
9786
+ settleMs: options.settle,
9787
+ fullImage: options.full === true ? true : undefined,
9788
+ imageScale: options.scale
9277
9789
  });
9278
9790
  if (options.json) {
9279
9791
  console.log(JSON.stringify(body, null, 2));
@@ -9283,6 +9795,10 @@ for (const kind of [
9283
9795
  if (body.resolved?.x != null && body.resolved?.y != null) {
9284
9796
  console.log(`resolved ${body.resolved.x},${body.resolved.y}`);
9285
9797
  }
9798
+ if (body.screenshot)
9799
+ printResultScreenshot(body.screenshot);
9800
+ if (body.phases)
9801
+ printActionPhases(body.phases);
9286
9802
  } catch (error) {
9287
9803
  fail(`action ${kind}`, error);
9288
9804
  }
@@ -9292,17 +9808,61 @@ addActionOptions(action.command("alert").description("Accept or dismiss a system
9292
9808
  try {
9293
9809
  const body = await client(String(options.baseUrl)).performAction({
9294
9810
  kind: "alert",
9295
- alertAction: options.dismiss ? "dismiss" : "accept"
9811
+ alertAction: options.dismiss ? "dismiss" : "accept",
9812
+ screenshot: options.screenshot === true ? true : undefined,
9813
+ settleMs: options.settle,
9814
+ fullImage: options.full === true ? true : undefined,
9815
+ imageScale: options.scale
9296
9816
  });
9297
9817
  if (options.json) {
9298
9818
  console.log(JSON.stringify(body, null, 2));
9299
9819
  return;
9300
9820
  }
9301
9821
  console.log(`ok ${body.kind}`);
9822
+ if (body.screenshot)
9823
+ printResultScreenshot(body.screenshot);
9302
9824
  } catch (error) {
9303
9825
  fail("action alert", error);
9304
9826
  }
9305
9827
  });
9828
+ addActionOptions(action.command("batch").description("Run known Actions from a JSON file or stdin (one Settle and Result screenshot at the end). Description-grounded steps are rejected.").argument("[file]", "JSON file of { steps: Action[] } or an Action array. Omit to read stdin.")).action(async (file, options) => {
9829
+ try {
9830
+ const raw = file ? await readFile2(file, "utf8") : await new Promise((resolve, reject) => {
9831
+ const chunks = [];
9832
+ process.stdin.on("data", (chunk) => {
9833
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
9834
+ });
9835
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
9836
+ process.stdin.on("error", reject);
9837
+ });
9838
+ const parsed = JSON.parse(raw);
9839
+ const steps = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && ("steps" in parsed) ? parsed.steps : null;
9840
+ if (!Array.isArray(steps) || steps.length === 0) {
9841
+ throw new Error("Batch JSON must be an Action array or { steps: Action[] }");
9842
+ }
9843
+ const body = await client(String(options.baseUrl)).performActionBatch({
9844
+ steps,
9845
+ settleMs: options.settle,
9846
+ fullImage: options.full === true ? true : undefined,
9847
+ imageScale: options.scale
9848
+ });
9849
+ if (options.json) {
9850
+ console.log(JSON.stringify(body, null, 2));
9851
+ return;
9852
+ }
9853
+ console.log(`batch ${body.ok ? "ok" : "failed"} ${body.completed}/${body.total}`);
9854
+ if (body.failedIndex != null)
9855
+ console.log(`failedIndex ${body.failedIndex}`);
9856
+ if (body.error)
9857
+ console.log(body.error);
9858
+ if (body.screenshot)
9859
+ printResultScreenshot(body.screenshot);
9860
+ if (!body.ok)
9861
+ process.exitCode = 1;
9862
+ } catch (error) {
9863
+ fail("action batch", error);
9864
+ }
9865
+ });
9306
9866
  var setup = program2.command("setup").description("Install Appium and the platform driver (xcuitest / uiautomator2)");
9307
9867
  setup.command("ios").description("Ensure Appium + xcuitest; optionally build and install WDA on a physical device").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--device <udid>", "Physical device UDID to install WebDriverAgent on").option("--kind <kind>", "Device kind: physical | simulator", "physical").option("--xcode <path>", "Xcode Contents/Developer path (DEVELOPER_DIR)").option("--team <teamId>", "Apple Development team ID").option("--identity <name>", 'Codesigning identity name, e.g. "Apple Development: …"').option("--force", "Force a full WebDriverAgent rebuild even when prep is reusable").action(async (options) => {
9308
9868
  try {
@@ -9588,13 +10148,14 @@ appsCmd.command("get").argument("<app>", "App prefix or id").option("--base-url
9588
10148
  console.log(`prefix: ${app.prefix}`);
9589
10149
  console.log(`name: ${app.name}`);
9590
10150
  console.log(`app_context: ${app.context || "(empty)"}`);
10151
+ console.log(`app_knowledge: ${app.knowledge || "(empty)"}`);
9591
10152
  console.log(`ios_bundle_id: ${app.iosBundleId || "(empty)"}`);
9592
10153
  console.log(`android_application_id: ${app.androidApplicationId || "(empty)"}`);
9593
10154
  } catch (error) {
9594
10155
  fail("apps get", error);
9595
10156
  }
9596
10157
  });
9597
- appsCmd.command("update").argument("<app>", "App prefix or id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--name <name>", "Display name").option("--prefix <prefix>", "CLI prefix").option("--context <text>", "App context / shared rules").option("--ios-bundle-id <id>", "iOS bundle id").option("--android-application-id <id>", "Android application id").option("--json", "Print raw JSON").action(async (appArg, options) => {
10158
+ appsCmd.command("update").argument("<app>", "App prefix or id").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--name <name>", "Display name").option("--prefix <prefix>", "CLI prefix").option("--context <text>", "App context / shared rules").option("--knowledge <text>", "App knowledge notes injected into agent runs").option("--ios-bundle-id <id>", "iOS bundle id").option("--android-application-id <id>", "Android application id").option("--json", "Print raw JSON").action(async (appArg, options) => {
9598
10159
  try {
9599
10160
  const c = client(options.baseUrl);
9600
10161
  const resolved = await resolveAppId(c, appArg);
@@ -9602,6 +10163,7 @@ appsCmd.command("update").argument("<app>", "App prefix or id").option("--base-u
9602
10163
  name: options.name,
9603
10164
  prefix: options.prefix,
9604
10165
  context: options.context,
10166
+ knowledge: options.knowledge,
9605
10167
  iosBundleId: options.iosBundleId,
9606
10168
  androidApplicationId: options.androidApplicationId
9607
10169
  });
@@ -9927,7 +10489,7 @@ runsCmd.command("delete").argument("<runId>", "Run id").option("--base-url <url>
9927
10489
  fail("runs delete", error);
9928
10490
  }
9929
10491
  });
9930
- runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--cases <numbers>", "Comma-separated case numbers, e.g. 1,2,5").option("--device <id>", "Device id (defaults to active session)").option("--platform <platform>", "ios | android (defaults to active session)").option("--build-id <id>", "Registered build id to install before run").option("--build-path <path>", "Absolute build path to register + install").option("--mode <mode>", "auto | script | agent (default: auto)").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--wait", "Wait until the run finishes (passed / errored / cancelled)").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS)).option("--github-output", "Write run_id and status to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs").action(async (appArg, options) => {
10492
+ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("--cases <numbers>", "Comma-separated case numbers, e.g. 1,2,5").option("--device <id>", "Device id (defaults to active session)").option("--platform <platform>", "ios | android (defaults to active session)").option("--build-id <id>", "Registered build id to install before run").option("--build-path <path>", "Absolute build path to register + install").option("--mode <mode>", "auto | script | agent (default: auto)").option("--screen-mode <mode>", "vision | tree — what the agent sees: screenshot only, or also the accessibility tree (default: vision)").option("--lane <lane>", "appium | direct | auto — Android / iOS sim auto → Direct").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print raw JSON").option("--wait", "Wait until the run finishes (passed / errored / cancelled)").option("--timeout <seconds>", "Wait timeout in seconds (default: 1800)", String(DEFAULT_RUN_WAIT_TIMEOUT_SECONDS)).option("--github-output", "Write run_id and status to $GITHUB_OUTPUT").option("--no-github-output", "Do not write GitHub step outputs").action(async (appArg, options) => {
9931
10493
  try {
9932
10494
  const c = client(options.baseUrl);
9933
10495
  const resolved = await resolveAppId(c, appArg);
@@ -9959,6 +10521,14 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
9959
10521
  if (options.mode && !executionMode) {
9960
10522
  throw new Error("--mode must be auto, script, or agent");
9961
10523
  }
10524
+ const screenMode = options.screenMode === "vision" || options.screenMode === "tree" ? options.screenMode : undefined;
10525
+ if (options.screenMode && !screenMode) {
10526
+ throw new Error("--screen-mode must be vision or tree");
10527
+ }
10528
+ const lane = options.lane === "appium" || options.lane === "direct" || options.lane === "auto" ? options.lane : undefined;
10529
+ if (options.lane && !lane) {
10530
+ throw new Error("--lane must be appium, direct, or auto");
10531
+ }
9962
10532
  let run = await c.createRun({
9963
10533
  appId: resolved.id,
9964
10534
  caseIds,
@@ -9966,7 +10536,9 @@ runsCmd.command("create").argument("<app>", "App prefix or id").requiredOption("
9966
10536
  platform,
9967
10537
  buildId: options.buildId,
9968
10538
  buildPath: options.buildPath,
9969
- executionMode
10539
+ executionMode,
10540
+ screenMode,
10541
+ lane
9970
10542
  });
9971
10543
  if (options.wait) {
9972
10544
  try {
@@ -10145,6 +10717,58 @@ scriptCmd.command("run").argument("<file>", "Path to a .yoqa.json / CaseScript J
10145
10717
  fail("script run", error);
10146
10718
  }
10147
10719
  });
10720
+ program2.command("benchmark").description("Compare Yoqa (and Argent, when installed) tap-to-result latency on one device").requiredOption("--device <id>", "Device UDID / serial").requiredOption("--platform <platform>", "ios | android").option("--repeats <n>", "Tap-to-result repeats (default: suite repeats)", (v) => Number(v)).option("--suite <path>", "Versioned suite JSON (default: built-in v1)").option("--tools <list>", "Comma-separated: yoqa,argent (default: yoqa)", "yoqa").option("--lane <lane>", "Yoqa Lane: appium | direct | auto", "auto").option("--arms <list>", "Yoqa Screen arms: vision,tree (default: suite arms)").option("--app <id>", "Catalog App id for Case arms (required when the suite has cases)").option("--out <path>", "Write the JSON report to this path").option("--base-url <url>", "Runner base URL", runnerBaseUrl()).option("--json", "Print JSON instead of the table").action(async (options) => {
10721
+ try {
10722
+ const platform = options.platform;
10723
+ if (platform !== "ios" && platform !== "android") {
10724
+ throw new Error("--platform must be ios or android");
10725
+ }
10726
+ const lane = options.lane === "appium" || options.lane === "direct" || options.lane === "auto" ? options.lane : undefined;
10727
+ if (options.lane && !lane)
10728
+ throw new Error("--lane must be appium, direct, or auto");
10729
+ const suite = options.suite ? parseBenchmarkSuite(JSON.parse(await readFile2(options.suite, "utf8"))) : DEFAULT_BENCHMARK_SUITE;
10730
+ const arms = options.arms ? parseBenchmarkArms(options.arms.split(",").map((item) => item.trim())) : suite.arms;
10731
+ const repeats = options.repeats && options.repeats > 0 ? options.repeats : suite.repeats;
10732
+ const tools = options.tools.split(",").map((t) => t.trim());
10733
+ const c = client(options.baseUrl);
10734
+ const runs = [];
10735
+ if (tools.includes("yoqa")) {
10736
+ for (const screenMode of arms) {
10737
+ runs.push(await runLatencyScenario(yoqaBenchmarkDriver(c, {
10738
+ deviceId: options.device,
10739
+ platform,
10740
+ lane,
10741
+ screenMode,
10742
+ appId: options.app
10743
+ }), { repeats, platform, suite, screenMode }));
10744
+ }
10745
+ }
10746
+ if (tools.includes("argent")) {
10747
+ console.error("Argent driver: install the argent CLI and re-run; skipping for now.");
10748
+ }
10749
+ const report = summarizeLatency({
10750
+ recordedAt: new Date().toISOString(),
10751
+ repeats,
10752
+ suiteVersion: suite.version,
10753
+ runs
10754
+ });
10755
+ const recommendation = recommendScreenDefault(report.results.find((row) => row.screenMode === "vision"), report.results.find((row) => row.screenMode === "tree"));
10756
+ if (options.out) {
10757
+ await writeFile3(options.out, `${JSON.stringify({ ...report, recommendation }, null, 2)}
10758
+ `);
10759
+ }
10760
+ if (options.json)
10761
+ console.log(JSON.stringify({ ...report, recommendation }, null, 2));
10762
+ else {
10763
+ console.log(formatLatencyTable(report));
10764
+ if (arms.includes("tree")) {
10765
+ console.log(`default ${recommendation.screenMode} — ${recommendation.reason}`);
10766
+ }
10767
+ }
10768
+ } catch (error) {
10769
+ fail("benchmark", error);
10770
+ }
10771
+ });
10148
10772
  function buildProgram() {
10149
10773
  return program2;
10150
10774
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoqa/cli",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
4
4
  "description": "Yoqa CLI — drive local mobile QA from the terminal or CI (talks to the Yoqa runner over HTTP)",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "url": "git+https://github.com/Aris-ngoy/qa-agent.git",
19
19
  "directory": "packages/cli"
20
20
  },
21
- "homepage": "https://yoqa.mintlify.app/docs/cli",
21
+ "homepage": "https://yoqa.arisn.dev/docs/cli",
22
22
  "bugs": {
23
23
  "url": "https://github.com/Aris-ngoy/qa-agent/issues"
24
24
  },
@@ -38,7 +38,7 @@
38
38
  "health": "bun run src/main.ts health"
39
39
  },
40
40
  "dependencies": {
41
- "@yoqa/runner": "0.3.27"
41
+ "@yoqa/runner": "0.3.29"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^22.15.30",