@yoqa/runner 1.0.0 → 1.0.3

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 (2) hide show
  1. package/dist/index.js +1794 -420
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10112,7 +10112,7 @@ var require_bignumber = __commonJS((exports, module) => {
10112
10112
  return arr.reverse();
10113
10113
  }
10114
10114
  return function(str, baseIn, baseOut, sign, callerIsToString) {
10115
- var alphabet, d, e, k, r, x, xc, y, i = str.indexOf("."), dp = DECIMAL_PLACES, rm5 = ROUNDING_MODE;
10115
+ var alphabet, d, e, k, r, x, xc, y, i = str.indexOf("."), dp = DECIMAL_PLACES, rm6 = ROUNDING_MODE;
10116
10116
  if (i >= 0) {
10117
10117
  k = POW_PRECISION;
10118
10118
  POW_PRECISION = 0;
@@ -10135,7 +10135,7 @@ var require_bignumber = __commonJS((exports, module) => {
10135
10135
  x.c = xc;
10136
10136
  x.e = e;
10137
10137
  x.s = sign;
10138
- x = div(x, y, dp, rm5, baseOut);
10138
+ x = div(x, y, dp, rm6, baseOut);
10139
10139
  xc = x.c;
10140
10140
  r = x.r;
10141
10141
  e = x.e;
@@ -10144,7 +10144,7 @@ var require_bignumber = __commonJS((exports, module) => {
10144
10144
  i = xc[d];
10145
10145
  k = baseOut / 2;
10146
10146
  r = r || d < 0 || xc[d + 1] != null;
10147
- r = rm5 < 4 ? (i != null || r) && (rm5 == 0 || rm5 == (x.s < 0 ? 3 : 2)) : i > k || i == k && (rm5 == 4 || r || rm5 == 6 && xc[d - 1] & 1 || rm5 == (x.s < 0 ? 8 : 7));
10147
+ r = rm6 < 4 ? (i != null || r) && (rm6 == 0 || rm6 == (x.s < 0 ? 3 : 2)) : i > k || i == k && (rm6 == 4 || r || rm6 == 6 && xc[d - 1] & 1 || rm6 == (x.s < 0 ? 8 : 7));
10148
10148
  if (d < 1 || !xc[0]) {
10149
10149
  str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0);
10150
10150
  } else {
@@ -10206,7 +10206,7 @@ var require_bignumber = __commonJS((exports, module) => {
10206
10206
  for (;!a[0] && a.length > 1; a.splice(0, 1))
10207
10207
  ;
10208
10208
  }
10209
- return function(x, y, dp, rm5, base) {
10209
+ return function(x, y, dp, rm6, base) {
10210
10210
  var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0, yL, yz, s = x.s == y.s ? 1 : -1, xc = x.c, yc = y.c;
10211
10211
  if (!xc || !xc[0] || !yc || !yc[0]) {
10212
10212
  return new BigNumber2(!x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN : xc && xc[0] == 0 || !yc ? s * 0 : s / 0);
@@ -10306,7 +10306,7 @@ var require_bignumber = __commonJS((exports, module) => {
10306
10306
  if (base == BASE) {
10307
10307
  for (i = 1, s = qc[0];s >= 10; s /= 10, i++)
10308
10308
  ;
10309
- round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm5, more);
10309
+ round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm6, more);
10310
10310
  } else {
10311
10311
  q.e = e;
10312
10312
  q.r = +more;
@@ -10314,12 +10314,12 @@ var require_bignumber = __commonJS((exports, module) => {
10314
10314
  return q;
10315
10315
  };
10316
10316
  }();
10317
- function format(n, i, rm5, id) {
10317
+ function format(n, i, rm6, id) {
10318
10318
  var c0, e, ne2, len, str;
10319
- if (rm5 == null)
10320
- rm5 = ROUNDING_MODE;
10319
+ if (rm6 == null)
10320
+ rm6 = ROUNDING_MODE;
10321
10321
  else
10322
- intCheck(rm5, 0, 8);
10322
+ intCheck(rm6, 0, 8);
10323
10323
  if (!n.c)
10324
10324
  return n.toString();
10325
10325
  c0 = n.c[0];
@@ -10328,7 +10328,7 @@ var require_bignumber = __commonJS((exports, module) => {
10328
10328
  str = coeffToString(n.c);
10329
10329
  str = id == 1 || id == 2 && (ne2 <= TO_EXP_NEG || ne2 >= TO_EXP_POS) ? toExponential(str, ne2) : toFixedPoint(str, ne2, "0");
10330
10330
  } else {
10331
- n = round(new BigNumber2(n), i, rm5);
10331
+ n = round(new BigNumber2(n), i, rm6);
10332
10332
  e = n.e;
10333
10333
  str = coeffToString(n.c);
10334
10334
  len = str.length;
@@ -10409,7 +10409,7 @@ var require_bignumber = __commonJS((exports, module) => {
10409
10409
  x.c = x.e = null;
10410
10410
  };
10411
10411
  }();
10412
- function round(x, sd, rm5, r) {
10412
+ function round(x, sd, rm6, r) {
10413
10413
  var d, i, j, k, n, ni, rd, xc = x.c, pows10 = POWS_TEN;
10414
10414
  if (xc) {
10415
10415
  out: {
@@ -10444,7 +10444,7 @@ var require_bignumber = __commonJS((exports, module) => {
10444
10444
  }
10445
10445
  }
10446
10446
  r = r || sd < 0 || xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]);
10447
- r = rm5 < 4 ? (rd || r) && (rm5 == 0 || rm5 == (x.s < 0 ? 3 : 2)) : rd > 5 || rd == 5 && (rm5 == 4 || r || rm5 == 6 && (i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10 & 1 || rm5 == (x.s < 0 ? 8 : 7));
10447
+ r = rm6 < 4 ? (rd || r) && (rm6 == 0 || rm6 == (x.s < 0 ? 3 : 2)) : rd > 5 || rd == 5 && (rm6 == 4 || r || rm6 == 6 && (i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10 & 1 || rm6 == (x.s < 0 ? 8 : 7));
10448
10448
  if (sd < 1 || !xc[0]) {
10449
10449
  xc.length = 0;
10450
10450
  if (r) {
@@ -10516,15 +10516,15 @@ var require_bignumber = __commonJS((exports, module) => {
10516
10516
  P.comparedTo = function(y, b) {
10517
10517
  return compare(this, new BigNumber2(y, b));
10518
10518
  };
10519
- P.decimalPlaces = P.dp = function(dp, rm5) {
10519
+ P.decimalPlaces = P.dp = function(dp, rm6) {
10520
10520
  var c, n, v, x = this;
10521
10521
  if (dp != null) {
10522
10522
  intCheck(dp, 0, MAX);
10523
- if (rm5 == null)
10524
- rm5 = ROUNDING_MODE;
10523
+ if (rm6 == null)
10524
+ rm6 = ROUNDING_MODE;
10525
10525
  else
10526
- intCheck(rm5, 0, 8);
10527
- return round(new BigNumber2(x), dp + x.e + 1, rm5);
10526
+ intCheck(rm6, 0, 8);
10527
+ return round(new BigNumber2(x), dp + x.e + 1, rm6);
10528
10528
  }
10529
10529
  if (!(c = x.c))
10530
10530
  return null;
@@ -10623,13 +10623,13 @@ var require_bignumber = __commonJS((exports, module) => {
10623
10623
  y = ONE.div(y);
10624
10624
  return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y;
10625
10625
  };
10626
- P.integerValue = function(rm5) {
10626
+ P.integerValue = function(rm6) {
10627
10627
  var n = new BigNumber2(this);
10628
- if (rm5 == null)
10629
- rm5 = ROUNDING_MODE;
10628
+ if (rm6 == null)
10629
+ rm6 = ROUNDING_MODE;
10630
10630
  else
10631
- intCheck(rm5, 0, 8);
10632
- return round(n, n.e + 1, rm5);
10631
+ intCheck(rm6, 0, 8);
10632
+ return round(n, n.e + 1, rm6);
10633
10633
  };
10634
10634
  P.isEqualTo = P.eq = function(y, b) {
10635
10635
  return compare(this, new BigNumber2(y, b)) === 0;
@@ -10866,15 +10866,15 @@ var require_bignumber = __commonJS((exports, module) => {
10866
10866
  }
10867
10867
  return normalise(y, xc, ye);
10868
10868
  };
10869
- P.precision = P.sd = function(sd, rm5) {
10869
+ P.precision = P.sd = function(sd, rm6) {
10870
10870
  var c, n, v, x = this;
10871
10871
  if (sd != null && sd !== !!sd) {
10872
10872
  intCheck(sd, 1, MAX);
10873
- if (rm5 == null)
10874
- rm5 = ROUNDING_MODE;
10873
+ if (rm6 == null)
10874
+ rm6 = ROUNDING_MODE;
10875
10875
  else
10876
- intCheck(rm5, 0, 8);
10877
- return round(new BigNumber2(x), sd, rm5);
10876
+ intCheck(rm6, 0, 8);
10877
+ return round(new BigNumber2(x), sd, rm6);
10878
10878
  }
10879
10879
  if (!(c = x.c))
10880
10880
  return null;
@@ -10951,36 +10951,36 @@ var require_bignumber = __commonJS((exports, module) => {
10951
10951
  }
10952
10952
  return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m);
10953
10953
  };
10954
- P.toExponential = function(dp, rm5) {
10954
+ P.toExponential = function(dp, rm6) {
10955
10955
  if (dp != null) {
10956
10956
  intCheck(dp, 0, MAX);
10957
10957
  dp++;
10958
10958
  }
10959
- return format(this, dp, rm5, 1);
10959
+ return format(this, dp, rm6, 1);
10960
10960
  };
10961
- P.toFixed = function(dp, rm5) {
10961
+ P.toFixed = function(dp, rm6) {
10962
10962
  if (dp != null) {
10963
10963
  intCheck(dp, 0, MAX);
10964
10964
  dp = dp + this.e + 1;
10965
10965
  }
10966
- return format(this, dp, rm5);
10966
+ return format(this, dp, rm6);
10967
10967
  };
10968
- P.toFormat = function(dp, rm5, format2) {
10968
+ P.toFormat = function(dp, rm6, format2) {
10969
10969
  var str, x = this;
10970
10970
  if (format2 == null) {
10971
- if (dp != null && rm5 && typeof rm5 == "object") {
10972
- format2 = rm5;
10973
- rm5 = null;
10971
+ if (dp != null && rm6 && typeof rm6 == "object") {
10972
+ format2 = rm6;
10973
+ rm6 = null;
10974
10974
  } else if (dp && typeof dp == "object") {
10975
10975
  format2 = dp;
10976
- dp = rm5 = null;
10976
+ dp = rm6 = null;
10977
10977
  } else {
10978
10978
  format2 = FORMAT;
10979
10979
  }
10980
10980
  } else if (typeof format2 != "object") {
10981
10981
  throw Error(bignumberError + "Argument not an object: " + format2);
10982
10982
  }
10983
- str = x.toFixed(dp, rm5);
10983
+ str = x.toFixed(dp, rm6);
10984
10984
  if (x.c) {
10985
10985
  var i, arr = str.split("."), g1 = +format2.groupSize, g2 = +format2.secondaryGroupSize, groupSeparator = format2.groupSeparator || "", intPart = arr[0], fractionPart = arr[1], isNeg = x.s < 0, intDigits = isNeg ? intPart.slice(1) : intPart, len = intDigits.length;
10986
10986
  if (g2) {
@@ -11048,10 +11048,10 @@ var require_bignumber = __commonJS((exports, module) => {
11048
11048
  P.toNumber = function() {
11049
11049
  return +valueOf(this);
11050
11050
  };
11051
- P.toPrecision = function(sd, rm5) {
11051
+ P.toPrecision = function(sd, rm6) {
11052
11052
  if (sd != null)
11053
11053
  intCheck(sd, 1, MAX);
11054
- return format(this, sd, rm5, 2);
11054
+ return format(this, sd, rm6, 2);
11055
11055
  };
11056
11056
  P.toString = function(b) {
11057
11057
  var str, n = this, s = n.s, e = n.e;
@@ -14526,7 +14526,7 @@ var require_getCredentials = __commonJS((exports) => {
14526
14526
  var fs = __require("fs");
14527
14527
  var util_1 = __require("util");
14528
14528
  var errorWithCode_1 = require_errorWithCode();
14529
- var readFile3 = fs.readFile ? (0, util_1.promisify)(fs.readFile) : async () => {
14529
+ var readFile4 = fs.readFile ? (0, util_1.promisify)(fs.readFile) : async () => {
14530
14530
  throw new errorWithCode_1.ErrorWithCode("use key rather than keyFile.", "MISSING_CREDENTIALS");
14531
14531
  };
14532
14532
  var ExtensionFiles;
@@ -14545,7 +14545,7 @@ var require_getCredentials = __commonJS((exports) => {
14545
14545
  this.keyFilePath = keyFilePath;
14546
14546
  }
14547
14547
  async getCredentials() {
14548
- const key = await readFile3(this.keyFilePath, "utf8");
14548
+ const key = await readFile4(this.keyFilePath, "utf8");
14549
14549
  let body;
14550
14550
  try {
14551
14551
  body = JSON.parse(key);
@@ -14568,7 +14568,7 @@ var require_getCredentials = __commonJS((exports) => {
14568
14568
  this.keyFilePath = keyFilePath;
14569
14569
  }
14570
14570
  async getCredentials() {
14571
- const privateKey = await readFile3(this.keyFilePath, "utf8");
14571
+ const privateKey = await readFile4(this.keyFilePath, "utf8");
14572
14572
  return { privateKey };
14573
14573
  }
14574
14574
  }
@@ -15766,7 +15766,7 @@ var require_filesubjecttokensupplier = __commonJS((exports) => {
15766
15766
  exports.FileSubjectTokenSupplier = undefined;
15767
15767
  var util_1 = __require("util");
15768
15768
  var fs = __require("fs");
15769
- var readFile3 = (0, util_1.promisify)(fs.readFile ?? (() => {}));
15769
+ var readFile4 = (0, util_1.promisify)(fs.readFile ?? (() => {}));
15770
15770
  var realpath = (0, util_1.promisify)(fs.realpath ?? (() => {}));
15771
15771
  var lstat = (0, util_1.promisify)(fs.lstat ?? (() => {}));
15772
15772
 
@@ -15793,7 +15793,7 @@ var require_filesubjecttokensupplier = __commonJS((exports) => {
15793
15793
  throw err;
15794
15794
  }
15795
15795
  let subjectToken;
15796
- const rawText = await readFile3(parsedFilePath, { encoding: "utf8" });
15796
+ const rawText = await readFile4(parsedFilePath, { encoding: "utf8" });
15797
15797
  if (this.formatType === "text") {
15798
15798
  subjectToken = rawText;
15799
15799
  } else if (this.formatType === "json" && this.subjectTokenFieldName) {
@@ -22280,6 +22280,7 @@ var apps = sqliteTable("apps", {
22280
22280
  iosBundleId: text("ios_bundle_id").notNull().default(""),
22281
22281
  iosAppStoreId: text("ios_app_store_id").notNull().default(""),
22282
22282
  androidApplicationId: text("android_application_id").notNull().default(""),
22283
+ webUrl: text("web_url").notNull().default(""),
22283
22284
  appiumCaps: text("appium_caps").notNull().default("[]"),
22284
22285
  createdAt: integer("created_at").notNull(),
22285
22286
  updatedAt: integer("updated_at").notNull()
@@ -22338,6 +22339,8 @@ var runs = sqliteTable("runs", {
22338
22339
  requestedLane: text("requested_lane"),
22339
22340
  lane: text("lane"),
22340
22341
  laneWarning: text("lane_warning"),
22342
+ browser: text("browser"),
22343
+ viewport: text("viewport"),
22341
22344
  recordVideo: integer("record_video").notNull().default(0),
22342
22345
  error: text("error"),
22343
22346
  createdAt: integer("created_at").notNull(),
@@ -22389,6 +22392,7 @@ function ensureSchema(sqlite) {
22389
22392
  ios_bundle_id TEXT NOT NULL DEFAULT '',
22390
22393
  ios_app_store_id TEXT NOT NULL DEFAULT '',
22391
22394
  android_application_id TEXT NOT NULL DEFAULT '',
22395
+ web_url TEXT NOT NULL DEFAULT '',
22392
22396
  appium_caps TEXT NOT NULL DEFAULT '[]',
22393
22397
  created_at INTEGER NOT NULL,
22394
22398
  updated_at INTEGER NOT NULL
@@ -22516,6 +22520,7 @@ function ensureSchema(sqlite) {
22516
22520
  migrateProvidersTable(sqlite);
22517
22521
  migrateAppsPrefix(sqlite);
22518
22522
  migrateAppsKnowledge(sqlite);
22523
+ migrateAppsWebUrl(sqlite);
22519
22524
  migrateCaseScripts(sqlite);
22520
22525
  migrateRunExecutionMode(sqlite);
22521
22526
  migrateRunScreenMode(sqlite);
@@ -22558,6 +22563,8 @@ function migrateRunLane(sqlite) {
22558
22563
  addColumnIfMissing(sqlite, "runs", "requested_lane", "requested_lane TEXT", runCols);
22559
22564
  addColumnIfMissing(sqlite, "runs", "lane", "lane TEXT", runCols);
22560
22565
  addColumnIfMissing(sqlite, "runs", "lane_warning", "lane_warning TEXT", runCols);
22566
+ addColumnIfMissing(sqlite, "runs", "browser", "browser TEXT", runCols);
22567
+ addColumnIfMissing(sqlite, "runs", "viewport", "viewport TEXT", runCols);
22561
22568
  }
22562
22569
  function migrateRecording(sqlite) {
22563
22570
  const caseCols = tableColumns(sqlite, "cases");
@@ -22684,6 +22691,12 @@ function migrateAppsKnowledge(sqlite) {
22684
22691
  return;
22685
22692
  addColumnIfMissing(sqlite, "apps", "knowledge", "knowledge TEXT NOT NULL DEFAULT ''", existing);
22686
22693
  }
22694
+ function migrateAppsWebUrl(sqlite) {
22695
+ const existing = tableColumns(sqlite, "apps");
22696
+ if (existing.size === 0)
22697
+ return;
22698
+ addColumnIfMissing(sqlite, "apps", "web_url", "web_url TEXT NOT NULL DEFAULT ''", existing);
22699
+ }
22687
22700
  function getCatalogDbPath() {
22688
22701
  return process.env.YOQA_DB_PATH ?? DEFAULT_DB_PATH;
22689
22702
  }
@@ -26688,13 +26701,20 @@ var healthResponseSchema = exports_external.object({
26688
26701
  version: exports_external.string(),
26689
26702
  uptimeMs: exports_external.number().nonnegative()
26690
26703
  });
26691
- var devicePlatformSchema = exports_external.union([exports_external.literal("ios"), exports_external.literal("android")]);
26704
+ var devicePlatformSchema = exports_external.union([
26705
+ exports_external.literal("ios"),
26706
+ exports_external.literal("android"),
26707
+ exports_external.literal("web")
26708
+ ]);
26692
26709
  var laneNameSchema = exports_external.literal("direct");
26693
26710
  var deviceKindSchema = exports_external.union([
26694
26711
  exports_external.literal("physical"),
26695
26712
  exports_external.literal("simulator"),
26696
- exports_external.literal("emulator")
26713
+ exports_external.literal("emulator"),
26714
+ exports_external.literal("browser")
26697
26715
  ]);
26716
+ var VIEWPORT_PRESET_NAMES = ["desktop", "mobile"];
26717
+ var viewportPresetSchema = exports_external.enum(VIEWPORT_PRESET_NAMES);
26698
26718
  var deviceSchema = exports_external.object({
26699
26719
  id: exports_external.string().min(1),
26700
26720
  name: exports_external.string().min(1),
@@ -26834,13 +26854,15 @@ var catalogAppSchema = exports_external.object({
26834
26854
  iosBundleId: exports_external.string(),
26835
26855
  iosAppStoreId: exports_external.string(),
26836
26856
  androidApplicationId: exports_external.string(),
26857
+ webUrl: exports_external.string().default(""),
26837
26858
  capabilities: exports_external.array(capabilitySchema),
26838
26859
  createdAt: exports_external.number().int().nonnegative(),
26839
26860
  updatedAt: exports_external.number().int().nonnegative()
26840
26861
  });
26841
26862
  var createAppRequestSchema = exports_external.object({
26842
26863
  name: exports_external.string().min(1),
26843
- prefix: exports_external.string().min(1).optional()
26864
+ prefix: exports_external.string().min(1).optional(),
26865
+ webUrl: exports_external.string().optional()
26844
26866
  });
26845
26867
  var updateAppRequestSchema = exports_external.object({
26846
26868
  name: exports_external.string().min(1).optional(),
@@ -26850,6 +26872,7 @@ var updateAppRequestSchema = exports_external.object({
26850
26872
  iosBundleId: exports_external.string().optional(),
26851
26873
  iosAppStoreId: exports_external.string().optional(),
26852
26874
  androidApplicationId: exports_external.string().optional(),
26875
+ webUrl: exports_external.string().optional(),
26853
26876
  capabilities: exports_external.array(capabilitySchema).optional()
26854
26877
  });
26855
26878
  var listAppsResponseSchema = exports_external.object({
@@ -26898,6 +26921,25 @@ var caseScriptDragSchema = exports_external.object({
26898
26921
  durationMs: exports_external.number().min(50).max(5000).optional(),
26899
26922
  reason: exports_external.string().optional()
26900
26923
  });
26924
+ var KEY_NAMES = [
26925
+ "Enter",
26926
+ "Tab",
26927
+ "Escape",
26928
+ "Backspace",
26929
+ "Delete",
26930
+ "ArrowUp",
26931
+ "ArrowDown",
26932
+ "ArrowLeft",
26933
+ "ArrowRight",
26934
+ "Home",
26935
+ "End",
26936
+ "PageUp",
26937
+ "PageDown",
26938
+ "Space"
26939
+ ];
26940
+ var keyNameSchema = exports_external.enum(KEY_NAMES);
26941
+ var KEY_MODIFIERS = ["Shift", "Control", "Alt", "Meta"];
26942
+ var keyModifierSchema = exports_external.enum(KEY_MODIFIERS);
26901
26943
  function caseScriptAppIdSchema(type) {
26902
26944
  return exports_external.object({
26903
26945
  type: exports_external.literal(type),
@@ -26905,60 +26947,99 @@ function caseScriptAppIdSchema(type) {
26905
26947
  reason: exports_external.string().optional()
26906
26948
  });
26907
26949
  }
26950
+ var caseScriptTypeSchema = exports_external.object({
26951
+ type: exports_external.literal("type"),
26952
+ text: exports_external.string(),
26953
+ reason: exports_external.string().optional()
26954
+ });
26955
+ var caseScriptWaitSchema = exports_external.object({
26956
+ type: exports_external.literal("wait"),
26957
+ ms: exports_external.number().min(0).max(1e4),
26958
+ reason: exports_external.string().optional()
26959
+ });
26960
+ var caseScriptAssertionSchema = exports_external.union([exports_external.literal("visible"), exports_external.literal("not-visible")]).default("visible");
26961
+ var caseScriptAssertSchema = exports_external.object({
26962
+ type: exports_external.literal("assert"),
26963
+ assertion: caseScriptAssertionSchema,
26964
+ text: exports_external.string().min(1),
26965
+ timeoutMs: exports_external.number().min(0).max(120000).optional(),
26966
+ reason: exports_external.string().optional()
26967
+ });
26968
+ var caseScriptAlertSchema = exports_external.object({
26969
+ type: exports_external.literal("alert"),
26970
+ alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).default("accept"),
26971
+ reason: exports_external.string().optional()
26972
+ });
26973
+ var caseScriptBackgroundAppSchema = exports_external.object({
26974
+ type: exports_external.literal("background-app"),
26975
+ seconds: exports_external.number().min(0).max(120).optional(),
26976
+ reason: exports_external.string().optional()
26977
+ });
26978
+ var caseScriptOpenUrlSchema = exports_external.object({
26979
+ type: exports_external.literal("open-url"),
26980
+ url: exports_external.string().min(1),
26981
+ reason: exports_external.string().optional()
26982
+ });
26983
+ var caseScriptKeySchema = exports_external.object({
26984
+ type: exports_external.literal("key"),
26985
+ key: keyNameSchema,
26986
+ modifiers: exports_external.array(keyModifierSchema).min(1).optional(),
26987
+ reason: exports_external.string().optional()
26988
+ });
26989
+ var caseScriptGestureSchema = exports_external.union([
26990
+ caseScriptTapSchema,
26991
+ caseScriptSwipeSchema,
26992
+ caseScriptDragSchema,
26993
+ caseScriptTypeSchema,
26994
+ caseScriptWaitSchema,
26995
+ caseScriptAssertSchema,
26996
+ caseScriptAlertSchema,
26997
+ caseScriptAppIdSchema("activate-app"),
26998
+ caseScriptAppIdSchema("terminate-app"),
26999
+ caseScriptAppIdSchema("restart-app"),
27000
+ caseScriptBackgroundAppSchema,
27001
+ caseScriptOpenUrlSchema,
27002
+ caseScriptKeySchema
27003
+ ]);
27004
+ var BRANCH_DEFAULT_TIMEOUT_MS = 2000;
27005
+ var caseScriptStepsSchema = exports_external.array(exports_external.lazy(() => caseScriptActionSchema));
27006
+ var caseScriptBranchSchema = exports_external.object({
27007
+ type: exports_external.literal("branch"),
27008
+ text: exports_external.string().min(1).optional(),
27009
+ id: exports_external.string().min(1).optional(),
27010
+ assertion: caseScriptAssertionSchema,
27011
+ timeoutMs: exports_external.number().min(0).max(120000).optional(),
27012
+ then: caseScriptStepsSchema.min(1),
27013
+ else: caseScriptStepsSchema.optional().transform((actions) => actions?.length ? actions : undefined),
27014
+ reason: exports_external.string().optional()
27015
+ }).superRefine((value, ctx) => {
27016
+ if (value.text == null === (value.id == null)) {
27017
+ ctx.addIssue({
27018
+ code: exports_external.ZodIssueCode.custom,
27019
+ message: "branch requires exactly one of text or id"
27020
+ });
27021
+ }
27022
+ });
27023
+ var caseScriptActionSchema = exports_external.union([
27024
+ caseScriptGestureSchema,
27025
+ caseScriptBranchSchema
27026
+ ]);
26908
27027
  var caseScriptActionSchemaByType = {
26909
27028
  tap: caseScriptTapSchema,
26910
27029
  swipe: caseScriptSwipeSchema,
26911
27030
  drag: caseScriptDragSchema,
26912
- type: exports_external.object({
26913
- type: exports_external.literal("type"),
26914
- text: exports_external.string(),
26915
- reason: exports_external.string().optional()
26916
- }),
26917
- wait: exports_external.object({
26918
- type: exports_external.literal("wait"),
26919
- ms: exports_external.number().min(0).max(1e4),
26920
- reason: exports_external.string().optional()
26921
- }),
26922
- assert: exports_external.object({
26923
- type: exports_external.literal("assert"),
26924
- assertion: exports_external.union([exports_external.literal("visible"), exports_external.literal("not-visible")]).default("visible"),
26925
- text: exports_external.string().min(1),
26926
- timeoutMs: exports_external.number().min(0).max(120000).optional(),
26927
- reason: exports_external.string().optional()
26928
- }),
26929
- alert: exports_external.object({
26930
- type: exports_external.literal("alert"),
26931
- alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).default("accept"),
26932
- reason: exports_external.string().optional()
26933
- }),
27031
+ type: caseScriptTypeSchema,
27032
+ wait: caseScriptWaitSchema,
27033
+ assert: caseScriptAssertSchema,
27034
+ alert: caseScriptAlertSchema,
26934
27035
  "activate-app": caseScriptAppIdSchema("activate-app"),
26935
27036
  "terminate-app": caseScriptAppIdSchema("terminate-app"),
26936
27037
  "restart-app": caseScriptAppIdSchema("restart-app"),
26937
- "background-app": exports_external.object({
26938
- type: exports_external.literal("background-app"),
26939
- seconds: exports_external.number().min(0).max(120).optional(),
26940
- reason: exports_external.string().optional()
26941
- }),
26942
- "open-url": exports_external.object({
26943
- type: exports_external.literal("open-url"),
26944
- url: exports_external.string().min(1),
26945
- reason: exports_external.string().optional()
26946
- })
27038
+ "background-app": caseScriptBackgroundAppSchema,
27039
+ "open-url": caseScriptOpenUrlSchema,
27040
+ key: caseScriptKeySchema,
27041
+ branch: caseScriptBranchSchema
26947
27042
  };
26948
- var caseScriptActionSchema = exports_external.union([
26949
- caseScriptActionSchemaByType.tap,
26950
- caseScriptActionSchemaByType.swipe,
26951
- caseScriptActionSchemaByType.drag,
26952
- caseScriptActionSchemaByType.type,
26953
- caseScriptActionSchemaByType.wait,
26954
- caseScriptActionSchemaByType.assert,
26955
- caseScriptActionSchemaByType.alert,
26956
- caseScriptActionSchemaByType["activate-app"],
26957
- caseScriptActionSchemaByType["terminate-app"],
26958
- caseScriptActionSchemaByType["restart-app"],
26959
- caseScriptActionSchemaByType["background-app"],
26960
- caseScriptActionSchemaByType["open-url"]
26961
- ]);
26962
27043
  var caseScriptSchema = exports_external.object({
26963
27044
  version: exports_external.literal(1),
26964
27045
  sourceRunId: exports_external.string().min(1).optional(),
@@ -27269,6 +27350,8 @@ var runSchema = exports_external.object({
27269
27350
  screenMode: runScreenModeSchema.optional(),
27270
27351
  lane: laneNameSchema.optional(),
27271
27352
  laneWarning: exports_external.string().min(1).optional(),
27353
+ browser: exports_external.string().min(1).optional(),
27354
+ viewport: viewportPresetSchema.optional(),
27272
27355
  error: exports_external.string().nullable(),
27273
27356
  createdAt: exports_external.number().int().nonnegative(),
27274
27357
  startedAt: exports_external.number().int().nonnegative().nullable(),
@@ -27287,7 +27370,8 @@ var connectDeviceRequestSchema = exports_external.object({
27287
27370
  platform: devicePlatformSchema,
27288
27371
  bundleId: exports_external.string().min(1).optional(),
27289
27372
  appPackage: exports_external.string().min(1).optional(),
27290
- lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional()
27373
+ lane: exports_external.union([laneNameSchema, exports_external.literal("auto")]).optional(),
27374
+ viewport: viewportPresetSchema.optional()
27291
27375
  });
27292
27376
  var streamStateSchema = exports_external.union([
27293
27377
  exports_external.literal("ready"),
@@ -27305,7 +27389,8 @@ var activeDeviceResponseSchema = exports_external.object({
27305
27389
  heldByRun: exports_external.boolean().optional(),
27306
27390
  heldByRunId: exports_external.string().min(1).optional(),
27307
27391
  lane: laneNameSchema.optional(),
27308
- laneWarning: exports_external.string().min(1).optional()
27392
+ laneWarning: exports_external.string().min(1).optional(),
27393
+ viewport: viewportPresetSchema.optional()
27309
27394
  });
27310
27395
  var retargetDeviceRequestSchema = exports_external.object({
27311
27396
  bundleId: exports_external.string().min(1).optional(),
@@ -27352,7 +27437,8 @@ var actionKindSchema = exports_external.union([
27352
27437
  exports_external.literal("restart-app"),
27353
27438
  exports_external.literal("background-app"),
27354
27439
  exports_external.literal("open-url"),
27355
- exports_external.literal("alert")
27440
+ exports_external.literal("alert"),
27441
+ exports_external.literal("key")
27356
27442
  ]);
27357
27443
  var actionRequestSchema = exports_external.object({
27358
27444
  kind: actionKindSchema,
@@ -27370,6 +27456,8 @@ var actionRequestSchema = exports_external.object({
27370
27456
  url: exports_external.string().optional(),
27371
27457
  alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
27372
27458
  seconds: exports_external.number().optional(),
27459
+ key: keyNameSchema.optional(),
27460
+ modifiers: exports_external.array(keyModifierSchema).optional(),
27373
27461
  screenshot: exports_external.boolean().optional(),
27374
27462
  settleMs: exports_external.number().int().min(0).max(1e4).optional(),
27375
27463
  fullImage: exports_external.boolean().optional(),
@@ -27451,6 +27539,58 @@ var listBuildsResponseSchema = exports_external.object({
27451
27539
 
27452
27540
  // ../../packages/runner-client/src/wait-for-run.ts
27453
27541
  var DEFAULT_WAIT_FOR_RUN_TIMEOUT_MS = 30 * 60 * 1000;
27542
+
27543
+ // ../../node_modules/yaml/dist/index.js
27544
+ var composer = require_composer();
27545
+ var Document = require_Document();
27546
+ var Schema2 = require_Schema();
27547
+ var errors2 = require_errors();
27548
+ var Alias = require_Alias();
27549
+ var identity = require_identity();
27550
+ var Pair = require_Pair();
27551
+ var Scalar = require_Scalar();
27552
+ var YAMLMap = require_YAMLMap();
27553
+ var YAMLSeq = require_YAMLSeq();
27554
+ var cst = require_cst();
27555
+ var lexer = require_lexer();
27556
+ var lineCounter = require_line_counter();
27557
+ var parser = require_parser();
27558
+ var publicApi = require_public_api();
27559
+ var visit = require_visit();
27560
+ var $Composer = composer.Composer;
27561
+ var $Document = Document.Document;
27562
+ var $Schema = Schema2.Schema;
27563
+ var $YAMLError = errors2.YAMLError;
27564
+ var $YAMLParseError = errors2.YAMLParseError;
27565
+ var $YAMLWarning = errors2.YAMLWarning;
27566
+ var $Alias = Alias.Alias;
27567
+ var $isAlias = identity.isAlias;
27568
+ var $isCollection = identity.isCollection;
27569
+ var $isDocument = identity.isDocument;
27570
+ var $isMap = identity.isMap;
27571
+ var $isNode = identity.isNode;
27572
+ var $isPair = identity.isPair;
27573
+ var $isScalar = identity.isScalar;
27574
+ var $isSeq = identity.isSeq;
27575
+ var $Pair = Pair.Pair;
27576
+ var $Scalar = Scalar.Scalar;
27577
+ var $YAMLMap = YAMLMap.YAMLMap;
27578
+ var $YAMLSeq = YAMLSeq.YAMLSeq;
27579
+ var $Lexer = lexer.Lexer;
27580
+ var $LineCounter = lineCounter.LineCounter;
27581
+ var $Parser = parser.Parser;
27582
+ var $parse = publicApi.parse;
27583
+ var $parseAllDocuments = publicApi.parseAllDocuments;
27584
+ var $parseDocument = publicApi.parseDocument;
27585
+ var $stringify = publicApi.stringify;
27586
+ var $visit = visit.visit;
27587
+ var $visitAsync = visit.visitAsync;
27588
+
27589
+ // ../../packages/runner-client/src/keys.ts
27590
+ function formatKeyChord(key, modifiers = []) {
27591
+ return [...modifiers, key].join("+");
27592
+ }
27593
+
27454
27594
  // ../../packages/runner-client/src/xml-entities.ts
27455
27595
  var NAMED = {
27456
27596
  amp: "&",
@@ -27482,6 +27622,10 @@ function shellSingleQuote(value) {
27482
27622
  }
27483
27623
  function formatActionShellLine(action) {
27484
27624
  const parts = ["yoqa", "action", action.kind];
27625
+ if (action.kind === "key" && action.key)
27626
+ parts.push(action.key);
27627
+ if (action.modifiers?.length)
27628
+ parts.push("--modifiers", action.modifiers.join(","));
27485
27629
  if (action.id) {
27486
27630
  parts.push("--id", shellSingleQuote(action.id));
27487
27631
  }
@@ -27522,8 +27666,14 @@ function formatSleepShellLine(seconds) {
27522
27666
  const rounded = Number.isInteger(safe) ? String(safe) : String(Number(safe.toFixed(3)));
27523
27667
  return `sleep ${rounded}`;
27524
27668
  }
27669
+ function assertTargetFlags(target) {
27670
+ return [
27671
+ ...target.text != null ? ["--text", shellSingleQuote(target.text.trim())] : [],
27672
+ ...target.id != null ? ["--id", shellSingleQuote(target.id.trim())] : []
27673
+ ];
27674
+ }
27525
27675
  function formatAssertShellLine(input) {
27526
- const parts = ["yoqa", "assert", input.assertion, "--text", shellSingleQuote(input.text.trim())];
27676
+ const parts = ["yoqa", "assert", input.assertion, ...assertTargetFlags(input)];
27527
27677
  const timeout = input.timeoutSeconds ?? 5;
27528
27678
  if (timeout !== 5) {
27529
27679
  parts.push("--timeout", String(timeout));
@@ -27551,6 +27701,11 @@ function screenHasText(elements, text2) {
27551
27701
  return label.includes(needle) || type.includes(needle);
27552
27702
  });
27553
27703
  }
27704
+ function screenHasTarget(elements, target) {
27705
+ if (target.id != null)
27706
+ return findElementById(elements, target.id) != null;
27707
+ return screenHasText(elements, target.text ?? "");
27708
+ }
27554
27709
  function findElementByLabel(elements, label) {
27555
27710
  const needle = normalizedLabel(label);
27556
27711
  if (!needle)
@@ -27588,63 +27743,104 @@ function elementCenterNorm(element) {
27588
27743
  y: Math.round(element.y + element.height / 2)
27589
27744
  };
27590
27745
  }
27591
- // ../../node_modules/yaml/dist/index.js
27592
- var composer = require_composer();
27593
- var Document = require_Document();
27594
- var Schema2 = require_Schema();
27595
- var errors2 = require_errors();
27596
- var Alias = require_Alias();
27597
- var identity = require_identity();
27598
- var Pair = require_Pair();
27599
- var Scalar = require_Scalar();
27600
- var YAMLMap = require_YAMLMap();
27601
- var YAMLSeq = require_YAMLSeq();
27602
- var cst = require_cst();
27603
- var lexer = require_lexer();
27604
- var lineCounter = require_line_counter();
27605
- var parser = require_parser();
27606
- var publicApi = require_public_api();
27607
- var visit = require_visit();
27608
- var $Composer = composer.Composer;
27609
- var $Document = Document.Document;
27610
- var $Schema = Schema2.Schema;
27611
- var $YAMLError = errors2.YAMLError;
27612
- var $YAMLParseError = errors2.YAMLParseError;
27613
- var $YAMLWarning = errors2.YAMLWarning;
27614
- var $Alias = Alias.Alias;
27615
- var $isAlias = identity.isAlias;
27616
- var $isCollection = identity.isCollection;
27617
- var $isDocument = identity.isDocument;
27618
- var $isMap = identity.isMap;
27619
- var $isNode = identity.isNode;
27620
- var $isPair = identity.isPair;
27621
- var $isScalar = identity.isScalar;
27622
- var $isSeq = identity.isSeq;
27623
- var $Pair = Pair.Pair;
27624
- var $Scalar = Scalar.Scalar;
27625
- var $YAMLMap = YAMLMap.YAMLMap;
27626
- var $YAMLSeq = YAMLSeq.YAMLSeq;
27627
- var $Lexer = lexer.Lexer;
27628
- var $LineCounter = lineCounter.LineCounter;
27629
- var $Parser = parser.Parser;
27630
- var $parse = publicApi.parse;
27631
- var $parseAllDocuments = publicApi.parseAllDocuments;
27632
- var $parseDocument = publicApi.parseDocument;
27633
- var $stringify = publicApi.stringify;
27634
- var $visit = visit.visit;
27635
- var $visitAsync = visit.visitAsync;
27636
27746
 
27637
27747
  // ../../packages/runner-client/src/case-script-file.ts
27638
- var ACTION_TYPES = Object.keys(caseScriptActionSchemaByType);
27639
- var TEXT_SHORT_FORM_FIELDS = new Set(["label", "text", "alertAction", "url", "appId"]);
27748
+ var ACTION_TYPES = Object.keys(caseScriptActionSchemaByType).filter((type) => type !== "branch");
27749
+ var BRANCH_KEY = "if";
27750
+ var ITEM_NAMES = [...ACTION_TYPES, BRANCH_KEY];
27751
+ var TEXT_SHORT_FORM_FIELDS = new Set(["label", "text", "alertAction", "url", "appId", "key"]);
27640
27752
  var TOP_LEVEL_KEYS = new Set(["version", "name", "sourceRunId", "savedAt", "actions"]);
27753
+
27754
+ // ../../packages/runner-client/src/screen-check.ts
27755
+ var SCREEN_CHECK_POLL_MS = 400;
27756
+ function abortError() {
27757
+ return Object.assign(new Error("Aborted"), { name: "AbortError" });
27758
+ }
27759
+ function describeScreenCheck(check) {
27760
+ return check.id != null ? `${check.assertion} id: ${check.id}` : `${check.assertion}: ${check.text}`;
27761
+ }
27762
+ function screenCheckMissMessage(options) {
27763
+ const seconds = Math.round(options.timeoutMs / 1000);
27764
+ const [kind, target] = options.id != null ? ["id", options.id] : ["text", options.text];
27765
+ return options.assertion === "visible" ? `Expected visible ${kind} not found within ${seconds}s: ${target}` : `Unexpected ${kind} still visible after ${seconds}s: ${target}`;
27766
+ }
27767
+ async function checkScreen(options) {
27768
+ const pollMs = options.pollMs ?? SCREEN_CHECK_POLL_MS;
27769
+ const deadline = options.now() + options.timeoutMs;
27770
+ for (;; ) {
27771
+ if (options.isAborted?.())
27772
+ throw abortError();
27773
+ let readError = null;
27774
+ try {
27775
+ const { elements } = await options.readScreen();
27776
+ const found = screenHasTarget(elements, options);
27777
+ if (options.assertion === "visible" ? found : !found)
27778
+ return { met: true };
27779
+ } catch (error) {
27780
+ if (options.isDeadSession?.(error))
27781
+ throw error;
27782
+ readError = error;
27783
+ }
27784
+ if (options.now() >= deadline) {
27785
+ if (readError) {
27786
+ const seconds = Math.round(options.timeoutMs / 1000);
27787
+ const message = readError instanceof Error ? readError.message : String(readError);
27788
+ throw new Error(`Could not read the screen within ${seconds}s: ${message}`);
27789
+ }
27790
+ return { met: false };
27791
+ }
27792
+ await options.sleep(pollMs);
27793
+ }
27794
+ }
27795
+
27796
+ // ../../packages/runner-client/src/script-format.ts
27797
+ var BRANCH_HELPER = "yoqa_branch";
27798
+ var BRANCH_HELPER_LINES = [
27799
+ "# A Branch's check: 0 runs `then`, 1 runs `else`, anything else stops the script.",
27800
+ `${BRANCH_HELPER}() {`,
27801
+ " local status=0",
27802
+ ' "$@" || status=$?',
27803
+ ' [ "$status" -le 1 ] || exit "$status"',
27804
+ ' return "$status"',
27805
+ "}",
27806
+ ""
27807
+ ];
27808
+ // ../../packages/runner-client/src/viewport.ts
27809
+ var VIEWPORT_PRESETS = {
27810
+ desktop: { name: "desktop", width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
27811
+ mobile: { name: "mobile", width: 390, height: 844, deviceScaleFactor: 3, mobile: true }
27812
+ };
27813
+ var DEFAULT_VIEWPORT_PRESET = "desktop";
27814
+ // ../../packages/runner-client/src/app-start-target.ts
27815
+ function appStartTarget(app, platform) {
27816
+ switch (platform) {
27817
+ case "ios":
27818
+ return app?.iosBundleId.trim() || undefined;
27819
+ case "android":
27820
+ return app?.androidApplicationId.trim() || undefined;
27821
+ case "web":
27822
+ return app?.webUrl.trim() || undefined;
27823
+ }
27824
+ }
27825
+ function startTargetRequest(platform, target) {
27826
+ if (!target)
27827
+ return {};
27828
+ switch (platform) {
27829
+ case "ios":
27830
+ return { bundleId: target };
27831
+ case "android":
27832
+ return { appPackage: target };
27833
+ case "web":
27834
+ return {};
27835
+ }
27836
+ }
27641
27837
  // src/domains/builds/application.ts
27642
27838
  import { basename, extname } from "path";
27643
27839
 
27644
27840
  // src/domains/devices/application.ts
27645
- import { mkdtemp, readFile, rm } from "fs/promises";
27646
- import { tmpdir } from "os";
27647
- import { join as join4 } from "path";
27841
+ import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2 } from "fs/promises";
27842
+ import { tmpdir as tmpdir2 } from "os";
27843
+ import { join as join5 } from "path";
27648
27844
 
27649
27845
  // src/domains/devices/android-identity.ts
27650
27846
  function matchAndroidIdentity(deviceId, connected, knownAvds = []) {
@@ -27672,6 +27868,153 @@ function parseAdbEmuAvdName(stdout) {
27672
27868
  return name || null;
27673
27869
  }
27674
27870
 
27871
+ // src/domains/devices/chromium-browsers.ts
27872
+ import { constants } from "fs";
27873
+ import { access, mkdtemp, readFile, rm } from "fs/promises";
27874
+ import { homedir as homedir4, tmpdir } from "os";
27875
+ import { join as join4 } from "path";
27876
+ var KNOWN_BROWSERS = [
27877
+ { id: "web-chrome", name: "Google Chrome", app: "Google Chrome" },
27878
+ { id: "web-edge", name: "Microsoft Edge", app: "Microsoft Edge" },
27879
+ { id: "web-brave", name: "Brave Browser", app: "Brave Browser" },
27880
+ { id: "web-chromium", name: "Chromium", app: "Chromium" }
27881
+ ];
27882
+ function installedBrowserIdOf(deviceId) {
27883
+ return KNOWN_BROWSERS.find((browser) => browser.id === deviceId)?.id ?? null;
27884
+ }
27885
+ function discoveryDeps() {
27886
+ return {
27887
+ platform: process.platform,
27888
+ home: homedir4(),
27889
+ isExecutable: (path) => access(path, constants.X_OK).then(() => true, () => false),
27890
+ readText: (path) => readFile(path, "utf8")
27891
+ };
27892
+ }
27893
+ var VERSION_RE = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/;
27894
+ async function bundleVersion(appPath, deps) {
27895
+ const plist = await deps.readText(join4(appPath, "Contents/Info.plist")).catch(() => "");
27896
+ return VERSION_RE.exec(plist)?.[1]?.trim() || null;
27897
+ }
27898
+ async function findInstalledBrowsers(deps = discoveryDeps()) {
27899
+ if (deps.platform !== "darwin")
27900
+ return [];
27901
+ const roots = ["/Applications", join4(deps.home, "Applications")];
27902
+ const found = await Promise.all(KNOWN_BROWSERS.map(async ({ id, name, app }) => {
27903
+ for (const root of roots) {
27904
+ const appPath = join4(root, `${app}.app`);
27905
+ const executable = join4(appPath, "Contents/MacOS", app);
27906
+ if (!await deps.isExecutable(executable))
27907
+ continue;
27908
+ return { id, name, version: await bundleVersion(appPath, deps), executable };
27909
+ }
27910
+ return null;
27911
+ }));
27912
+ return found.filter((browser) => browser !== null);
27913
+ }
27914
+ var running = new Set;
27915
+ async function listWebDevices(options = {}, deps = discoveryDeps()) {
27916
+ const includeUnavailable = options.includeUnavailable ?? true;
27917
+ const devices = [];
27918
+ for (const browser of await findInstalledBrowsers(deps)) {
27919
+ const isRunning = running.has(browser.id);
27920
+ if (!includeUnavailable && !isRunning)
27921
+ continue;
27922
+ devices.push({
27923
+ id: browser.id,
27924
+ name: browser.name,
27925
+ osVersion: browser.version ?? "unknown version",
27926
+ platform: "web",
27927
+ kind: "browser",
27928
+ state: isRunning ? "running" : "not running"
27929
+ });
27930
+ }
27931
+ return devices;
27932
+ }
27933
+ var READY_TIMEOUT_MS = 15000;
27934
+ var READY_POLL_MS = 50;
27935
+ var STOP_GRACE_MS = 2000;
27936
+ var LAUNCH_FLAGS = [
27937
+ "--remote-debugging-port=0",
27938
+ "--no-first-run",
27939
+ "--no-default-browser-check",
27940
+ "--disable-search-engine-choice-screen",
27941
+ "--disable-sync",
27942
+ "--disable-default-apps",
27943
+ "--use-mock-keychain",
27944
+ "--password-store=basic"
27945
+ ];
27946
+ function spawnBrowser(command) {
27947
+ return Bun.spawn(command, { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
27948
+ }
27949
+ async function readActivePort(profileDir) {
27950
+ const text2 = await readFile(join4(profileDir, "DevToolsActivePort"), "utf8").catch(() => "");
27951
+ const [port, path] = text2.split(`
27952
+ `).map((line) => line.trim());
27953
+ if (!port || !/^\d+$/.test(port) || !path?.startsWith("/"))
27954
+ return null;
27955
+ return `ws://127.0.0.1:${port}${path}`;
27956
+ }
27957
+ async function launchChromium(browser, deps = {}) {
27958
+ const spawn = deps.spawn ?? spawnBrowser;
27959
+ const readyTimeoutMs = deps.readyTimeoutMs ?? READY_TIMEOUT_MS;
27960
+ const profileDir = await mkdtemp(join4(tmpdir(), "yoqa-web-"));
27961
+ const removeProfile = () => rm(profileDir, { recursive: true, force: true });
27962
+ let proc;
27963
+ try {
27964
+ proc = spawn([
27965
+ browser.executable,
27966
+ `--user-data-dir=${profileDir}`,
27967
+ ...LAUNCH_FLAGS,
27968
+ "about:blank"
27969
+ ]);
27970
+ } catch (error) {
27971
+ await removeProfile();
27972
+ throw error;
27973
+ }
27974
+ let hasExited = false;
27975
+ const exited = proc.exited.then((code) => {
27976
+ hasExited = true;
27977
+ running.delete(browser.id);
27978
+ return code;
27979
+ });
27980
+ const stderr = proc.stderr ? new Response(proc.stderr).text().catch(() => "") : null;
27981
+ let stopping = null;
27982
+ const stop = () => {
27983
+ stopping ??= (async () => {
27984
+ if (!hasExited) {
27985
+ proc.kill("SIGTERM");
27986
+ const exitedInTime = await Promise.race([
27987
+ exited.then(() => true),
27988
+ Bun.sleep(STOP_GRACE_MS).then(() => false)
27989
+ ]);
27990
+ if (!exitedInTime)
27991
+ proc.kill("SIGKILL");
27992
+ }
27993
+ running.delete(browser.id);
27994
+ await removeProfile();
27995
+ })();
27996
+ return stopping;
27997
+ };
27998
+ const deadline = Date.now() + readyTimeoutMs;
27999
+ while (true) {
28000
+ const socketUrl = await readActivePort(profileDir);
28001
+ if (socketUrl) {
28002
+ running.add(browser.id);
28003
+ return { socketUrl, profileDir, exited, stop };
28004
+ }
28005
+ if (hasExited) {
28006
+ await stop();
28007
+ const output = (await stderr ?? "").trim();
28008
+ throw new Error(`${browser.name} exited before it opened a debug port${output ? `: ${output}` : ""}`);
28009
+ }
28010
+ if (Date.now() >= deadline) {
28011
+ await stop();
28012
+ throw new Error(`${browser.name} did not open a debug port within ${readyTimeoutMs} ms`);
28013
+ }
28014
+ await Promise.race([exited, Bun.sleep(READY_POLL_MS)]);
28015
+ }
28016
+ }
28017
+
27675
28018
  // src/domains/devices/devicectl.ts
27676
28019
  function developerMode(value) {
27677
28020
  const key = typeof value === "string" ? value : value && typeof value === "object" ? Object.keys(value)[0] : undefined;
@@ -27799,11 +28142,11 @@ async function resolveTool(candidates) {
27799
28142
  }
27800
28143
  async function resolveAdb() {
27801
28144
  const sdk = androidSdkRoot();
27802
- return resolveTool([...sdk ? [join4(sdk, "platform-tools/adb")] : [], "adb"]);
28145
+ return resolveTool([...sdk ? [join5(sdk, "platform-tools/adb")] : [], "adb"]);
27803
28146
  }
27804
28147
  async function resolveEmulator() {
27805
28148
  const sdk = androidSdkRoot();
27806
- return resolveTool([...sdk ? [join4(sdk, "emulator/emulator")] : [], "emulator"]);
28149
+ return resolveTool([...sdk ? [join5(sdk, "emulator/emulator")] : [], "emulator"]);
27807
28150
  }
27808
28151
  function formatIosVersion2(version2) {
27809
28152
  const trimmed = version2.trim();
@@ -27901,14 +28244,14 @@ async function readDevicectlDevices(jsonPath) {
27901
28244
  if (exitCode !== 0)
27902
28245
  return null;
27903
28246
  try {
27904
- return JSON.parse(await readFile(jsonPath, "utf8"));
28247
+ return JSON.parse(await readFile2(jsonPath, "utf8"));
27905
28248
  } catch {
27906
28249
  return null;
27907
28250
  }
27908
28251
  }
27909
28252
  async function listIosPhysicalDevices(includeUnavailable) {
27910
- const dir = await mkdtemp(join4(tmpdir(), "yoqa-devices-"));
27911
- const jsonPath = join4(dir, "devices.json");
28253
+ const dir = await mkdtemp2(join5(tmpdir2(), "yoqa-devices-"));
28254
+ const jsonPath = join5(dir, "devices.json");
27912
28255
  try {
27913
28256
  let parsed = await readDevicectlDevices(jsonPath);
27914
28257
  if (parsed === null)
@@ -27921,7 +28264,7 @@ async function listIosPhysicalDevices(includeUnavailable) {
27921
28264
  const devices = parseDevicectlDevices(parsed);
27922
28265
  return includeUnavailable ? devices : devices.filter((device) => !device.unavailableReason);
27923
28266
  } finally {
27924
- await rm(dir, { recursive: true, force: true });
28267
+ await rm2(dir, { recursive: true, force: true });
27925
28268
  }
27926
28269
  }
27927
28270
  async function listIosDevices(options = {}) {
@@ -27972,18 +28315,18 @@ function humanizeModel(model) {
27972
28315
  }
27973
28316
  async function findAvdConfigPath(avdName) {
27974
28317
  const home = process.env.HOME ?? "";
27975
- const roots = [join4(home, ".android/avd"), join4(home, "Library/Android/sdk/.android/avd")];
28318
+ const roots = [join5(home, ".android/avd"), join5(home, "Library/Android/sdk/.android/avd")];
27976
28319
  for (const root of roots) {
27977
- const direct = join4(root, `${avdName}.avd`, "config.ini");
28320
+ const direct = join5(root, `${avdName}.avd`, "config.ini");
27978
28321
  try {
27979
- await readFile(direct, "utf8");
28322
+ await readFile2(direct, "utf8");
27980
28323
  return direct;
27981
28324
  } catch {}
27982
28325
  try {
27983
28326
  const glob = new Bun.Glob("*.avd/config.ini");
27984
28327
  for await (const match of glob.scan({ cwd: root, onlyFiles: true })) {
27985
- const path = join4(root, match);
27986
- const text2 = await readFile(path, "utf8");
28328
+ const path = join5(root, match);
28329
+ const text2 = await readFile2(path, "utf8");
27987
28330
  const idLine = text2.split(`
27988
28331
  `).find((line) => line.startsWith("AvdId="));
27989
28332
  const id = idLine?.slice("AvdId=".length).trim();
@@ -27999,7 +28342,7 @@ async function readAvdConfig(avdName) {
27999
28342
  if (!path)
28000
28343
  return { api: null, deviceName: null };
28001
28344
  try {
28002
- const text2 = await readFile(path, "utf8");
28345
+ const text2 = await readFile2(path, "utf8");
28003
28346
  const imageLine = text2.split(`
28004
28347
  `).find((line) => line.includes("system-images") && line.includes("android-"));
28005
28348
  const apiMatch = imageLine?.match(/android-([\d.]+)/);
@@ -28140,9 +28483,14 @@ async function listAndroidDevices(options = {}) {
28140
28483
  return [...physical, ...emulators];
28141
28484
  }
28142
28485
  async function listDevices(platform, options = {}) {
28143
- if (platform === "ios")
28144
- return listIosDevices(options);
28145
- return listAndroidDevices(options);
28486
+ switch (platform) {
28487
+ case "ios":
28488
+ return listIosDevices(options);
28489
+ case "android":
28490
+ return listAndroidDevices(options);
28491
+ case "web":
28492
+ return listWebDevices(options);
28493
+ }
28146
28494
  }
28147
28495
 
28148
28496
  // src/domains/builds/application.ts
@@ -28252,32 +28600,44 @@ async function resolveBuildForRun(options) {
28252
28600
  }
28253
28601
  async function installBuildOnDevice(options) {
28254
28602
  const { build, deviceId, platform } = options;
28255
- if (platform === "ios") {
28256
- if (build.path.endsWith(".app") || build.path.includes(".app")) {
28257
- const proc2 = Bun.spawn(["xcrun", "simctl", "install", deviceId, build.path], {
28258
- stdout: "pipe",
28259
- stderr: "pipe"
28260
- });
28261
- const [stderr2, code2] = await Promise.all([new Response(proc2.stderr).text(), proc2.exited]);
28262
- if (code2 !== 0) {
28263
- throw new Error(`simctl install failed: ${stderr2.trim() || `exit ${code2}`}`);
28264
- }
28265
- return;
28603
+ switch (platform) {
28604
+ case "ios":
28605
+ return installIosBuild(build, deviceId);
28606
+ case "android":
28607
+ return installAndroidBuild(build, deviceId);
28608
+ case "web":
28609
+ throw new Error("A web device has no Builds to install");
28610
+ default:
28611
+ throw new Error(`Cannot install a build on ${platform}`);
28612
+ }
28613
+ }
28614
+ async function installIosBuild(build, deviceId) {
28615
+ if (build.path.endsWith(".app") || build.path.includes(".app")) {
28616
+ const proc = Bun.spawn(["xcrun", "simctl", "install", deviceId, build.path], {
28617
+ stdout: "pipe",
28618
+ stderr: "pipe"
28619
+ });
28620
+ const [stderr, code] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
28621
+ if (code !== 0) {
28622
+ throw new Error(`simctl install failed: ${stderr.trim() || `exit ${code}`}`);
28266
28623
  }
28267
- const which = Bun.which("ideviceinstaller");
28268
- if (which && build.path.endsWith(".ipa")) {
28269
- const proc2 = Bun.spawn([which, "-u", deviceId, "-i", build.path], {
28270
- stdout: "pipe",
28271
- stderr: "pipe"
28272
- });
28273
- const [stderr2, code2] = await Promise.all([new Response(proc2.stderr).text(), proc2.exited]);
28274
- if (code2 !== 0) {
28275
- throw new Error(`ideviceinstaller failed: ${stderr2.trim() || `exit ${code2}`}`);
28276
- }
28277
- return;
28624
+ return;
28625
+ }
28626
+ const which = Bun.which("ideviceinstaller");
28627
+ if (which && build.path.endsWith(".ipa")) {
28628
+ const proc = Bun.spawn([which, "-u", deviceId, "-i", build.path], {
28629
+ stdout: "pipe",
28630
+ stderr: "pipe"
28631
+ });
28632
+ const [stderr, code] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
28633
+ if (code !== 0) {
28634
+ throw new Error(`ideviceinstaller failed: ${stderr.trim() || `exit ${code}`}`);
28278
28635
  }
28279
- throw new Error(`Cannot install iOS build at ${build.path}. Use a .app on simulator or install .ipa via ideviceinstaller.`);
28636
+ return;
28280
28637
  }
28638
+ throw new Error(`Cannot install iOS build at ${build.path}. Use a .app on simulator or install .ipa via ideviceinstaller.`);
28639
+ }
28640
+ async function installAndroidBuild(build, deviceId) {
28281
28641
  const identity2 = await resolveAndroidIdentity(deviceId);
28282
28642
  const serial = identity2.udid;
28283
28643
  if (!serial) {
@@ -28491,10 +28851,10 @@ function normalizeVisionJson(value) {
28491
28851
  }
28492
28852
 
28493
28853
  // src/domains/providers/vision-model.ts
28494
- import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile } from "fs/promises";
28495
- import { homedir as homedir4 } from "os";
28496
- import { tmpdir as tmpdir2 } from "os";
28497
- import { join as join5 } from "path";
28854
+ import { mkdtemp as mkdtemp3, readFile as readFile3, rm as rm3, writeFile } from "fs/promises";
28855
+ import { homedir as homedir5 } from "os";
28856
+ import { tmpdir as tmpdir3 } from "os";
28857
+ import { join as join6 } from "path";
28498
28858
 
28499
28859
  // ../../node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider/dist/index.js
28500
28860
  var marker = "vercel.ai.error";
@@ -51171,9 +51531,9 @@ async function prepareVisionImage(pngBase64) {
51171
51531
  if (process.platform !== "darwin") {
51172
51532
  return { base64: pngBase64, mediaType: "image/png" };
51173
51533
  }
51174
- const dir = await mkdtemp2(join5(tmpdir2(), "yoqa-vision-"));
51175
- const inPath = join5(dir, "shot.png");
51176
- const outPath = join5(dir, "shot.jpg");
51534
+ const dir = await mkdtemp3(join6(tmpdir3(), "yoqa-vision-"));
51535
+ const inPath = join6(dir, "shot.png");
51536
+ const outPath = join6(dir, "shot.jpg");
51177
51537
  try {
51178
51538
  await writeFile(inPath, new Uint8Array(bytes));
51179
51539
  const proc = Bun.spawn([
@@ -51194,7 +51554,7 @@ async function prepareVisionImage(pngBase64) {
51194
51554
  if (code !== 0) {
51195
51555
  return { base64: pngBase64, mediaType: "image/png" };
51196
51556
  }
51197
- const jpeg = await readFile2(outPath);
51557
+ const jpeg = await readFile3(outPath);
51198
51558
  if (jpeg.byteLength === 0 || jpeg.byteLength >= bytes.byteLength) {
51199
51559
  return { base64: pngBase64, mediaType: "image/png" };
51200
51560
  }
@@ -51202,7 +51562,7 @@ async function prepareVisionImage(pngBase64) {
51202
51562
  } catch {
51203
51563
  return { base64: pngBase64, mediaType: "image/png" };
51204
51564
  } finally {
51205
- await rm2(dir, { recursive: true, force: true }).catch(() => {
51565
+ await rm3(dir, { recursive: true, force: true }).catch(() => {
51206
51566
  return;
51207
51567
  });
51208
51568
  }
@@ -51389,8 +51749,8 @@ function openCodeAuthPaths() {
51389
51749
  const paths = [];
51390
51750
  const xdg = process.env.XDG_DATA_HOME?.trim();
51391
51751
  if (xdg)
51392
- paths.push(join5(xdg, "opencode", "auth.json"));
51393
- paths.push(join5(homedir4(), ".local", "share", "opencode", "auth.json"));
51752
+ paths.push(join6(xdg, "opencode", "auth.json"));
51753
+ paths.push(join6(homedir5(), ".local", "share", "opencode", "auth.json"));
51394
51754
  return paths;
51395
51755
  }
51396
51756
  function apiKeyFromOpenCodeAuthEntry(value) {
@@ -60599,13 +60959,13 @@ function createAnthropic(options = {}) {
60599
60959
  var anthropic = createAnthropic();
60600
60960
 
60601
60961
  // src/domains/providers/drivers/probe.ts
60602
- import { constants } from "fs";
60603
- import { access } from "fs/promises";
60604
- import { homedir as homedir5 } from "os";
60605
- import { isAbsolute, join as join6 } from "path";
60962
+ import { constants as constants2 } from "fs";
60963
+ import { access as access2 } from "fs/promises";
60964
+ import { homedir as homedir6 } from "os";
60965
+ import { isAbsolute, join as join7 } from "path";
60606
60966
  async function pathExists(path) {
60607
60967
  try {
60608
- await access(path, constants.F_OK);
60968
+ await access2(path, constants2.F_OK);
60609
60969
  return true;
60610
60970
  } catch {
60611
60971
  return false;
@@ -60627,13 +60987,13 @@ async function which(binary) {
60627
60987
  `)[0]?.trim();
60628
60988
  return resolved || null;
60629
60989
  }
60630
- function knownHomeBinCandidates(binaryName, home = homedir5()) {
60990
+ function knownHomeBinCandidates(binaryName, home = homedir6()) {
60631
60991
  return [
60632
- join6(home, ".opencode", "bin", binaryName),
60633
- join6(home, ".grok", "bin", binaryName),
60634
- join6(home, ".antigravity", "antigravity", "bin", binaryName),
60635
- join6(home, ".local", "bin", binaryName),
60636
- join6(home, ".bun", "bin", binaryName),
60992
+ join7(home, ".opencode", "bin", binaryName),
60993
+ join7(home, ".grok", "bin", binaryName),
60994
+ join7(home, ".antigravity", "antigravity", "bin", binaryName),
60995
+ join7(home, ".local", "bin", binaryName),
60996
+ join7(home, ".bun", "bin", binaryName),
60637
60997
  `/opt/homebrew/bin/${binaryName}`,
60638
60998
  `/usr/local/bin/${binaryName}`
60639
60999
  ];
@@ -60898,9 +61258,9 @@ function resolveAgyModelId(raw, fallback = ANTIGRAVITY_DEFAULT_VISION_MODEL) {
60898
61258
  }
60899
61259
 
60900
61260
  // src/domains/providers/drivers/antigravity-vision.ts
60901
- import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile2 } from "fs/promises";
60902
- import { tmpdir as tmpdir3 } from "os";
60903
- import { join as join7 } from "path";
61261
+ import { mkdtemp as mkdtemp4, rm as rm4, writeFile as writeFile2 } from "fs/promises";
61262
+ import { tmpdir as tmpdir4 } from "os";
61263
+ import { join as join8 } from "path";
60904
61264
 
60905
61265
  // ../../node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider/dist/index.js
60906
61266
  var marker29 = "vercel.ai.error";
@@ -70375,8 +70735,8 @@ async function completeWithAgyCli(input, repairHint) {
70375
70735
  const image = input.image ?? await prepareVisionImage(input.imageBase64);
70376
70736
  const model = resolveAgyModelId(input.auth.defaultModel);
70377
70737
  const ext = image.mediaType === "image/jpeg" ? "jpg" : "png";
70378
- const dir = await mkdtemp3(join7(tmpdir3(), "yoqa-agy-"));
70379
- const shotPath = join7(dir, `shot.${ext}`);
70738
+ const dir = await mkdtemp4(join8(tmpdir4(), "yoqa-agy-"));
70739
+ const shotPath = join8(dir, `shot.${ext}`);
70380
70740
  try {
70381
70741
  await writeFile2(shotPath, new Uint8Array(Buffer.from(image.base64, "base64")));
70382
70742
  const userText = [
@@ -70416,7 +70776,7 @@ ${result.stderr}`.trim();
70416
70776
  }
70417
70777
  throw error40;
70418
70778
  } finally {
70419
- await rm3(dir, { recursive: true, force: true }).catch(() => {
70779
+ await rm4(dir, { recursive: true, force: true }).catch(() => {
70420
70780
  return;
70421
70781
  });
70422
70782
  }
@@ -78884,9 +79244,9 @@ ${result.stderr}`.toLowerCase();
78884
79244
  };
78885
79245
 
78886
79246
  // src/domains/providers/drivers/cursor-vision.ts
78887
- import { mkdtemp as mkdtemp4, rm as rm4, writeFile as writeFile3 } from "fs/promises";
78888
- import { tmpdir as tmpdir4 } from "os";
78889
- import { join as join8 } from "path";
79247
+ import { mkdtemp as mkdtemp5, rm as rm5, writeFile as writeFile3 } from "fs/promises";
79248
+ import { tmpdir as tmpdir5 } from "os";
79249
+ import { join as join9 } from "path";
78890
79250
  var CURSOR_DEFAULT_VISION_MODEL = "auto";
78891
79251
  var JSON_REPAIR_PROMPT3 = "Your previous reply was not valid JSON for this task. Reply again with ONLY one strict JSON object using double quotes for every key and string (no single quotes, no markdown, no prose).";
78892
79252
  function resolveCursorApiKey(auth) {
@@ -78899,8 +79259,8 @@ async function invokeCursorPrint(input) {
78899
79259
  }
78900
79260
  const model = input.auth.defaultModel?.trim() || CURSOR_DEFAULT_VISION_MODEL;
78901
79261
  const ext = input.mediaType === "image/jpeg" ? "jpg" : "png";
78902
- const dir = await mkdtemp4(join8(tmpdir4(), "yoqa-cursor-"));
78903
- const shotPath = join8(dir, `shot.${ext}`);
79262
+ const dir = await mkdtemp5(join9(tmpdir5(), "yoqa-cursor-"));
79263
+ const shotPath = join9(dir, `shot.${ext}`);
78904
79264
  const apiKey = resolveCursorApiKey(input.auth);
78905
79265
  try {
78906
79266
  await writeFile3(shotPath, new Uint8Array(Buffer.from(input.imageBase64, "base64")));
@@ -78937,7 +79297,7 @@ ${result.stderr}`.trim();
78937
79297
  }
78938
79298
  return result.stdout.trim() || combined;
78939
79299
  } finally {
78940
- await rm4(dir, { recursive: true, force: true }).catch(() => {
79300
+ await rm5(dir, { recursive: true, force: true }).catch(() => {
78941
79301
  return;
78942
79302
  });
78943
79303
  }
@@ -99029,8 +99389,8 @@ var openaiDriver = {
99029
99389
  };
99030
99390
 
99031
99391
  // src/domains/providers/drivers/opencode-config.ts
99032
- import { homedir as homedir6 } from "os";
99033
- import { join as join9 } from "path";
99392
+ import { homedir as homedir7 } from "os";
99393
+ import { join as join10 } from "path";
99034
99394
  var OPENAI_COMPATIBLE_PACKAGES = new Set([
99035
99395
  "@ai-sdk/openai-compatible",
99036
99396
  "@ai-sdk/openai",
@@ -99058,8 +99418,8 @@ function openCodeConfigPaths() {
99058
99418
  const paths = [];
99059
99419
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
99060
99420
  if (xdg)
99061
- paths.push(join9(xdg, "opencode", "opencode.json"));
99062
- paths.push(join9(homedir6(), ".config", "opencode", "opencode.json"));
99421
+ paths.push(join10(xdg, "opencode", "opencode.json"));
99422
+ paths.push(join10(homedir7(), ".config", "opencode", "opencode.json"));
99063
99423
  return paths;
99064
99424
  }
99065
99425
  function parseOpenCodeCompatibleProvider(config2, providerId) {
@@ -99880,6 +100240,7 @@ var agentDecisionSchema = exports_external.object({
99880
100240
  exports_external.literal("restart-app"),
99881
100241
  exports_external.literal("background-app"),
99882
100242
  exports_external.literal("open-url"),
100243
+ exports_external.literal("key"),
99883
100244
  exports_external.literal("assert"),
99884
100245
  exports_external.literal("verify"),
99885
100246
  exports_external.literal("done"),
@@ -99909,6 +100270,8 @@ var agentDecisionSchema = exports_external.object({
99909
100270
  appId: exports_external.string().min(1).optional(),
99910
100271
  url: exports_external.string().min(1).optional(),
99911
100272
  seconds: exports_external.number().optional(),
100273
+ key: keyNameSchema.optional(),
100274
+ modifiers: exports_external.array(keyModifierSchema).optional(),
99912
100275
  assertion: exports_external.union([exports_external.literal("visible"), exports_external.literal("not-visible")]).optional(),
99913
100276
  timeoutMs: exports_external.number().optional(),
99914
100277
  reason: exports_external.string().min(1),
@@ -99937,6 +100300,9 @@ var agentDecisionSchema = exports_external.object({
99937
100300
  if (value.type === "open-url" && !value.url?.trim()) {
99938
100301
  ctx.addIssue({ code: exports_external.ZodIssueCode.custom, message: "open-url requires url" });
99939
100302
  }
100303
+ if (value.type === "key" && !value.key) {
100304
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, message: "key requires key" });
100305
+ }
99940
100306
  if (value.type === "assert" && !value.text?.trim()) {
99941
100307
  ctx.addIssue({ code: exports_external.ZodIssueCode.custom, message: "assert requires text" });
99942
100308
  }
@@ -100000,7 +100366,7 @@ function releaseCanvasPoint(decision, elements) {
100000
100366
  return point;
100001
100367
  }
100002
100368
  var SNAP_RADIUS = 100;
100003
- var TAPPABLE_ROLE_RE = /button|link|cell|switch|checkbox|radio|tab|toggle|textfield|searchfield|segment|menuitem|stepper|slider|picker/i;
100369
+ var TAPPABLE_ROLE_RE = /button|link|cell|switch|checkbox|radio|tab|toggle|textfield|searchfield|segment|menuitem|stepper|slider|picker|textbox|searchbox|combobox|option/i;
100004
100370
  function isTappableElement(element) {
100005
100371
  if (isFullBleedElement(element))
100006
100372
  return false;
@@ -100223,6 +100589,12 @@ function decisionToActionRequest(decision, options = {}) {
100223
100589
  };
100224
100590
  case "open-url":
100225
100591
  return decision.url ? { kind: "open-url", url: decision.url } : null;
100592
+ case "key":
100593
+ return decision.key ? {
100594
+ kind: "key",
100595
+ key: decision.key,
100596
+ ...decision.modifiers?.length ? { modifiers: decision.modifiers } : {}
100597
+ } : null;
100226
100598
  default:
100227
100599
  return null;
100228
100600
  }
@@ -100315,6 +100687,8 @@ var APP_LIFECYCLE_SHAPES = `{"type":"activate-app","appId":"com.example.app","re
100315
100687
  {"type":"restart-app","appId":"com.example.app","reason":"...","thoughts":"..."}
100316
100688
  {"type":"background-app","seconds":3,"reason":"...","thoughts":"..."}
100317
100689
  {"type":"open-url","url":"https://example.com","reason":"...","thoughts":"..."}
100690
+ {"type":"key","key":"Enter","reason":"...","thoughts":"..."}
100691
+ {"type":"key","key":"Tab","modifiers":["Shift"],"reason":"...","thoughts":"..."}
100318
100692
  {"type":"assert","assertion":"visible|not-visible","text":"...","reason":"...","thoughts":"..."}
100319
100693
  {"type":"wait","ms":500-3000,"reason":"...","thoughts":"..."}
100320
100694
  {"type":"verify","reason":"...","thoughts":"..."}
@@ -100332,6 +100706,7 @@ var SWIPE_GUIDANCE = `Swipe direction is the FINGER movement (same as Inspector)
100332
100706
  - "left" / "right": horizontal paging
100333
100707
  When the case says "scroll up", emit direction "down". When it says "scroll down" or "to the bottom", emit direction "up". Never tap to scroll. Never emit wait as a stand-in for scrolling.`;
100334
100708
  var SHARED_RULES = `- activate-app / terminate-app / restart-app / open-url / background-app match yoqa action. Omit appId to use the catalog app id provided in the user message.
100709
+ - key presses one named key: ${KEY_NAMES.join("|")}, with optional modifiers ${KEY_MODIFIERS.join("|")}. Use it to submit a form (Enter), move focus (Tab), close a dialog or menu (Escape), or move through a list or combobox (arrows). If the device cannot press that key, the error says so; choose another action.
100335
100710
  - To scroll a list or page, emit swipe \u2014 never a tap in the middle of the screen as a stand-in for scrolling.
100336
100711
  - "Scroll until you cannot scroll anymore" / "to the bottom": keep emitting swipe (direction "up") until a later screenshot shows the same content. Then verify. Unchanged screens after taps do not count \u2014 taps do not scroll.
100337
100712
  - You are given ONE current instruction. Complete only that instruction, then return verify or done. Later instructions are hidden on purpose \u2014 do not invent, skip ahead, or combine them. Earlier finished instructions may be omitted \u2014 do not reconstruct them.
@@ -100453,6 +100828,9 @@ function formatRecentActions(actions) {
100453
100828
  if (action.type === "open-url") {
100454
100829
  return `${n}. open-url ${action.url ?? ""} \u2014 ${action.reason ?? ""}`;
100455
100830
  }
100831
+ if (action.type === "key") {
100832
+ return `${n}. key ${formatKeyChord(action.key ?? "?", action.modifiers)} \u2014 ${action.reason ?? ""}`;
100833
+ }
100456
100834
  if (action.type === "wait") {
100457
100835
  return `${n}. wait ${action.ms ?? "?"}ms \u2014 ${action.reason ?? ""}`;
100458
100836
  }
@@ -100745,6 +101123,15 @@ function buildScriptFromDecisions(decisions, sourceRunId) {
100745
101123
  url: decision.url,
100746
101124
  reason: decision.reason
100747
101125
  });
101126
+ } else if (decision.type === "key") {
101127
+ if (!decision.key)
101128
+ continue;
101129
+ actions.push({
101130
+ type: "key",
101131
+ key: decision.key,
101132
+ ...decision.modifiers?.length ? { modifiers: decision.modifiers } : {},
101133
+ reason: decision.reason
101134
+ });
100748
101135
  } else if (decision.type === "assert") {
100749
101136
  if (!decision.text?.trim())
100750
101137
  continue;
@@ -100807,6 +101194,7 @@ function mapApp(row) {
100807
101194
  iosBundleId: row.iosBundleId,
100808
101195
  iosAppStoreId: row.iosAppStoreId,
100809
101196
  androidApplicationId: row.androidApplicationId,
101197
+ webUrl: row.webUrl,
100810
101198
  capabilities: [],
100811
101199
  createdAt: row.createdAt,
100812
101200
  updatedAt: row.updatedAt
@@ -100981,6 +101369,7 @@ async function createApp(input) {
100981
101369
  if (!name30) {
100982
101370
  throw new CatalogValidationError("Application name is required");
100983
101371
  }
101372
+ const webUrl = parseWebUrl(input.webUrl ?? "");
100984
101373
  const db = getCatalogDb();
100985
101374
  const now = Date.now();
100986
101375
  const id = newId2("app");
@@ -100994,6 +101383,7 @@ async function createApp(input) {
100994
101383
  iosBundleId: "",
100995
101384
  iosAppStoreId: "",
100996
101385
  androidApplicationId: "",
101386
+ webUrl,
100997
101387
  appiumCaps: "[]",
100998
101388
  createdAt: now,
100999
101389
  updatedAt: now
@@ -101004,6 +101394,16 @@ async function createApp(input) {
101004
101394
  }
101005
101395
  return created;
101006
101396
  }
101397
+ function parseWebUrl(raw) {
101398
+ const webUrl = raw.trim();
101399
+ if (!webUrl)
101400
+ return "";
101401
+ const protocol = URL.canParse(webUrl) ? new URL(webUrl).protocol : null;
101402
+ if (protocol !== "http:" && protocol !== "https:") {
101403
+ throw new CatalogValidationError(`Web URL must be an http or https URL, not "${webUrl}"`);
101404
+ }
101405
+ return webUrl;
101406
+ }
101007
101407
  var MAX_APP_KNOWLEDGE_CHARS2 = 2048;
101008
101408
  async function updateApp(appId, input) {
101009
101409
  const db = getCatalogDb();
@@ -101018,6 +101418,7 @@ async function updateApp(appId, input) {
101018
101418
  if (input.knowledge !== undefined && input.knowledge.length > MAX_APP_KNOWLEDGE_CHARS2) {
101019
101419
  throw new CatalogValidationError(`App knowledge must be at most ${MAX_APP_KNOWLEDGE_CHARS2} characters`);
101020
101420
  }
101421
+ const webUrl = input.webUrl !== undefined ? parseWebUrl(input.webUrl) : existing.webUrl;
101021
101422
  const prefix = input.prefix !== undefined ? await allocatePrefix(input.prefix.trim() || name30, appId) : existing.prefix || await allocatePrefix(name30, appId);
101022
101423
  await db.update(apps).set({
101023
101424
  name: name30,
@@ -101027,6 +101428,7 @@ async function updateApp(appId, input) {
101027
101428
  iosBundleId: input.iosBundleId ?? existing.iosBundleId,
101028
101429
  iosAppStoreId: input.iosAppStoreId ?? existing.iosAppStoreId,
101029
101430
  androidApplicationId: input.androidApplicationId ?? existing.androidApplicationId,
101431
+ webUrl,
101030
101432
  updatedAt: Date.now()
101031
101433
  }).where(eq(apps.id, appId));
101032
101434
  const updated = await getApp(appId);
@@ -101279,7 +101681,7 @@ function abortAllMjpegProxies() {
101279
101681
 
101280
101682
  // src/domains/devices/android-devtools.ts
101281
101683
  import { existsSync as existsSync2 } from "fs";
101282
- import { join as join10 } from "path";
101684
+ import { join as join11 } from "path";
101283
101685
  function escapeAttr(value) {
101284
101686
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
101285
101687
  }
@@ -101307,13 +101709,13 @@ var DEVTOOLS_PACKAGE = "yoqa.android.devtools";
101307
101709
  var DEVTOOLS_INSTRUMENTATION = `${DEVTOOLS_PACKAGE}/.DevtoolsInstrumentation`;
101308
101710
  var DEVTOOLS_VERSION_CODE = 2;
101309
101711
  var DEVTOOLS_DEVICE_PORT = 7421;
101310
- var READY_TIMEOUT_MS = 1e4;
101712
+ var READY_TIMEOUT_MS2 = 1e4;
101311
101713
  var TREE_TIMEOUT_MS = 5000;
101312
101714
  function resolveDevtoolsApk() {
101313
101715
  const fromEnv = process.env.YOQA_ANDROID_DEVTOOLS_APK?.trim();
101314
101716
  if (fromEnv)
101315
101717
  return fromEnv;
101316
- const built = join10(import.meta.dir, "../../../../../native/android-devtools/app/build/outputs/apk/debug/app-debug.apk");
101718
+ const built = join11(import.meta.dir, "../../../../../native/android-devtools/app/build/outputs/apk/debug/app-debug.apk");
101317
101719
  return existsSync2(built) ? built : null;
101318
101720
  }
101319
101721
  async function adbOk(adb, args, label) {
@@ -101422,7 +101824,7 @@ async function startAndroidDevtools(context, deps = {}) {
101422
101824
  });
101423
101825
  };
101424
101826
  try {
101425
- await waitUntilReady(base, instrument, deps.readyTimeoutMs ?? READY_TIMEOUT_MS);
101827
+ await waitUntilReady(base, instrument, deps.readyTimeoutMs ?? READY_TIMEOUT_MS2);
101426
101828
  } catch (error40) {
101427
101829
  await stop();
101428
101830
  throw error40;
@@ -101435,9 +101837,38 @@ async function startAndroidDevtools(context, deps = {}) {
101435
101837
 
101436
101838
  // src/domains/devices/android-direct-lane.ts
101437
101839
  import { mkdir } from "fs/promises";
101438
- import { dirname as dirname2, join as join12 } from "path";
101840
+ import { dirname as dirname2, join as join13 } from "path";
101439
101841
 
101440
101842
  // src/domains/devices/adb-input.ts
101843
+ var ANDROID_KEYCODES = {
101844
+ Enter: 66,
101845
+ Tab: 61,
101846
+ Escape: 111,
101847
+ Backspace: 67,
101848
+ Delete: 112,
101849
+ ArrowUp: 19,
101850
+ ArrowDown: 20,
101851
+ ArrowLeft: 21,
101852
+ ArrowRight: 22,
101853
+ Home: 122,
101854
+ End: 123,
101855
+ PageUp: 92,
101856
+ PageDown: 93,
101857
+ Space: 62
101858
+ };
101859
+ var ANDROID_MODIFIER_KEYCODES = {
101860
+ Shift: 59,
101861
+ Control: 113,
101862
+ Alt: 57,
101863
+ Meta: 117
101864
+ };
101865
+ function adbKeyArgs(key, modifiers = []) {
101866
+ const code = String(ANDROID_KEYCODES[key]);
101867
+ if (modifiers.length === 0)
101868
+ return ["input", "keyevent", code];
101869
+ const held = modifiers.map((modifier) => String(ANDROID_MODIFIER_KEYCODES[modifier]));
101870
+ return ["input", "keycombination", ...held, code];
101871
+ }
101441
101872
  function parseWmSize(stdout) {
101442
101873
  const override = stdout.match(/Override size:\s*(\d+)x(\d+)/i);
101443
101874
  const physical = stdout.match(/Physical size:\s*(\d+)x(\d+)/i);
@@ -101546,6 +101977,21 @@ function reportLaneFallback(session, options, warning) {
101546
101977
  session.laneWarning = joinLaneWarnings(session.laneWarning, warning);
101547
101978
  options.onLaneWarning?.(warning);
101548
101979
  }
101980
+
101981
+ class UnsupportedKeyError extends Error {
101982
+ constructor(message) {
101983
+ super(message);
101984
+ this.name = "UnsupportedKeyError";
101985
+ }
101986
+ }
101987
+ function assertKeySupported(support, device, key, modifiers = []) {
101988
+ if (!support.keys.includes(key)) {
101989
+ throw new UnsupportedKeyError(`The ${key} key is not available ${device}; it can press ${support.keys.join(", ")}`);
101990
+ }
101991
+ if (modifiers.length > 0 && !support.modifiers) {
101992
+ throw new UnsupportedKeyError(`Modifier keys (${modifiers.join(", ")}) are not available ${device}; press ${key} without them`);
101993
+ }
101994
+ }
101549
101995
  var DEAD_SESSION_RE = /session does not exist|invalid session id|no such session|terminated or not started|session is either terminated/i;
101550
101996
 
101551
101997
  class DeadSessionError extends Error {
@@ -101555,6 +102001,13 @@ class DeadSessionError extends Error {
101555
102001
  }
101556
102002
  }
101557
102003
 
102004
+ class ActionUnsupportedError extends Error {
102005
+ constructor(message) {
102006
+ super(message);
102007
+ this.name = "ActionUnsupportedError";
102008
+ }
102009
+ }
102010
+
101558
102011
  class DeviceUnreachableError extends Error {
101559
102012
  unreachableMs;
101560
102013
  graceMs;
@@ -101593,7 +102046,7 @@ function createFrameLoop(capture, options = {}) {
101593
102046
  const now = () => performance.now();
101594
102047
  let latest = null;
101595
102048
  let fatal = null;
101596
- let running = false;
102049
+ let running2 = false;
101597
102050
  let kicked = false;
101598
102051
  let lastRead = 0;
101599
102052
  let waiters = [];
@@ -101632,12 +102085,12 @@ function createFrameLoop(capture, options = {}) {
101632
102085
  }
101633
102086
  };
101634
102087
  const run = async () => {
101635
- running = true;
102088
+ running2 = true;
101636
102089
  while (!fatal && (waiters.length > 0 || now() - lastRead < idleMs)) {
101637
102090
  if (!await captureOnce(true) && !fatal)
101638
102091
  await Bun.sleep(RETRY_AFTER_ERROR_MS);
101639
102092
  }
101640
- running = false;
102093
+ running2 = false;
101641
102094
  };
101642
102095
  return {
101643
102096
  read: (after = 0) => {
@@ -101649,12 +102102,12 @@ function createFrameLoop(capture, options = {}) {
101649
102102
  const pending = new Promise((resolve7, reject) => {
101650
102103
  waiters.push({ after, resolve: resolve7, reject });
101651
102104
  });
101652
- if (!running)
102105
+ if (!running2)
101653
102106
  run();
101654
102107
  return pending;
101655
102108
  },
101656
102109
  kick: () => {
101657
- if (fatal || !running || kicked)
102110
+ if (fatal || !running2 || kicked)
101658
102111
  return;
101659
102112
  kicked = true;
101660
102113
  captureOnce(false).finally(() => {
@@ -101705,9 +102158,9 @@ async function exitsWithin(proc, ms) {
101705
102158
 
101706
102159
  // src/domains/devices/screenshot-retention.ts
101707
102160
  import { readdir, stat, unlink } from "fs/promises";
101708
- import { homedir as homedir7 } from "os";
101709
- import { join as join11 } from "path";
101710
- var SCREENSHOT_DIR = join11(homedir7(), ".yoqa", "runs", "screenshots");
102161
+ import { homedir as homedir8 } from "os";
102162
+ import { join as join12 } from "path";
102163
+ var SCREENSHOT_DIR = join12(homedir8(), ".yoqa", "runs", "screenshots");
101711
102164
  var SHOT_FILE_RE = /^shot_\d+_[0-9a-f-]+\.png$/i;
101712
102165
  var DEFAULT_RETENTION_DAYS = 7;
101713
102166
  var DAY_MS = 24 * 60 * 60 * 1000;
@@ -101735,7 +102188,7 @@ async function pruneOldRunScreenshots(options = {}) {
101735
102188
  const names = (await readdir(dir)).filter((name30) => SHOT_FILE_RE.test(name30));
101736
102189
  const entries = [];
101737
102190
  for (const name30 of names) {
101738
- const info = await stat(join11(dir, name30));
102191
+ const info = await stat(join12(dir, name30));
101739
102192
  entries.push({ name: name30, mtimeMs: info.mtimeMs });
101740
102193
  }
101741
102194
  let removed = 0;
@@ -101744,7 +102197,7 @@ async function pruneOldRunScreenshots(options = {}) {
101744
102197
  now: options.now ?? Date.now()
101745
102198
  })) {
101746
102199
  try {
101747
- await unlink(join11(dir, name30));
102200
+ await unlink(join12(dir, name30));
101748
102201
  removed += 1;
101749
102202
  } catch {}
101750
102203
  }
@@ -101763,6 +102216,7 @@ async function readRemotePid(stdout) {
101763
102216
  throw new Error(`screenrecord: could not read its pid (${pid || "empty"})`);
101764
102217
  return pid;
101765
102218
  }
102219
+ var ANDROID_KEY_SUPPORT = { keys: KEY_NAMES, modifiers: true };
101766
102220
  var ADB_LOST_RE = /device '.*' not found|device offline|device not found|no devices\/emulators found/i;
101767
102221
  function adbLostDevice(result) {
101768
102222
  return result.exitCode === 127 || result.exitCode !== 0 && ADB_LOST_RE.test(result.stderr);
@@ -101881,7 +102335,7 @@ async function createAndroidDirectSession(options, deps = {}) {
101881
102335
  const screenshot = async () => {
101882
102336
  await mkdir(SCREENSHOT_DIR, { recursive: true });
101883
102337
  const frame = await captureFrame();
101884
- const path = join12(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
102338
+ const path = join13(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
101885
102339
  await Bun.write(path, Uint8Array.from(Buffer.from(frame.base64, "base64")));
101886
102340
  return { path, base64: frame.base64 };
101887
102341
  };
@@ -101904,9 +102358,9 @@ async function createAndroidDirectSession(options, deps = {}) {
101904
102358
  await adb(["-s", serial, "shell", "am", "force-stop", DEVTOOLS_PACKAGE]);
101905
102359
  }
101906
102360
  const stopDevtools = async () => {
101907
- const running = devtools;
102361
+ const running2 = devtools;
101908
102362
  devtools = null;
101909
- await running?.stop().catch(() => {
102363
+ await running2?.stop().catch(() => {
101910
102364
  return;
101911
102365
  });
101912
102366
  };
@@ -101980,6 +102434,12 @@ async function createAndroidDirectSession(options, deps = {}) {
101980
102434
  }
101981
102435
  });
101982
102436
  };
102437
+ const key = async (name30, modifiers = []) => {
102438
+ assertKeySupported(ANDROID_KEY_SUPPORT, "on Android", name30, modifiers);
102439
+ await lock.withLock(async () => {
102440
+ await shell2(adbKeyArgs(name30, modifiers), "input key");
102441
+ });
102442
+ };
101983
102443
  const activateApp = async (appId) => {
101984
102444
  await lock.withLock(async () => {
101985
102445
  lastAppId = appId;
@@ -102060,6 +102520,8 @@ async function createAndroidDirectSession(options, deps = {}) {
102060
102520
  swipe,
102061
102521
  drag: (x1, y1, x2, y2, durationMs = 800) => swipe(x1, y1, x2, y2, durationMs),
102062
102522
  type,
102523
+ keySupport: ANDROID_KEY_SUPPORT,
102524
+ key,
102063
102525
  activateApp,
102064
102526
  setTargetApp: (appId) => {
102065
102527
  lastAppId = appId;
@@ -102099,7 +102561,7 @@ async function createAndroidDirectSession(options, deps = {}) {
102099
102561
 
102100
102562
  // src/domains/devices/ios-device-lane.ts
102101
102563
  import { mkdir as mkdir5 } from "fs/promises";
102102
- import { join as join18 } from "path";
102564
+ import { join as join19 } from "path";
102103
102565
 
102104
102566
  // src/domains/devices/error-chain.ts
102105
102567
  var MAX_LINKS = 8;
@@ -102128,9 +102590,9 @@ function errorChain(error40) {
102128
102590
  }
102129
102591
 
102130
102592
  // src/domains/devices/frame-recorder.ts
102131
- import { mkdir as mkdir2, mkdtemp as mkdtemp5, rm as rm5 } from "fs/promises";
102132
- import { tmpdir as tmpdir5 } from "os";
102133
- import { dirname as dirname3, join as join13 } from "path";
102593
+ import { mkdir as mkdir2, mkdtemp as mkdtemp6, rm as rm6 } from "fs/promises";
102594
+ import { tmpdir as tmpdir6 } from "os";
102595
+ import { dirname as dirname3, join as join14 } from "path";
102134
102596
  var LAST_FRAME_HOLD_S = 0.5;
102135
102597
  async function encodeWithFfmpeg(ffmpeg, list, out) {
102136
102598
  const proc = Bun.spawn([
@@ -102178,36 +102640,36 @@ async function recordFrames(source, path, deps = realDeps) {
102178
102640
  if (!ffmpeg)
102179
102641
  throw new Error("ffmpeg not found on PATH");
102180
102642
  await mkdir2(dirname3(path), { recursive: true });
102181
- const dir = await mkdtemp5(join13(tmpdir5(), "yoqa-frames-"));
102643
+ const dir = await mkdtemp6(join14(tmpdir6(), "yoqa-frames-"));
102182
102644
  try {
102183
102645
  await source.start?.();
102184
102646
  } catch (error40) {
102185
- await rm5(dir, { recursive: true, force: true }).catch(() => {
102647
+ await rm6(dir, { recursive: true, force: true }).catch(() => {
102186
102648
  return;
102187
102649
  });
102188
102650
  throw error40;
102189
102651
  }
102190
102652
  const frames = [];
102191
- let running = true;
102653
+ let running2 = true;
102192
102654
  const pullOnce = async () => {
102193
102655
  try {
102194
102656
  for (const frame of await source.pull()) {
102195
- const file2 = join13(dir, `${String(frames.length).padStart(6, "0")}.${frame.ext}`);
102657
+ const file2 = join14(dir, `${String(frames.length).padStart(6, "0")}.${frame.ext}`);
102196
102658
  await Bun.write(file2, frame.bytes);
102197
102659
  frames.push({ file: file2, at: frame.at });
102198
102660
  }
102199
102661
  } catch {}
102200
102662
  };
102201
102663
  const loop = (async () => {
102202
- while (running) {
102664
+ while (running2) {
102203
102665
  await pullOnce();
102204
- if (running)
102666
+ if (running2)
102205
102667
  await Bun.sleep(source.intervalMs);
102206
102668
  }
102207
102669
  })();
102208
102670
  return {
102209
102671
  stop: async () => {
102210
- running = false;
102672
+ running2 = false;
102211
102673
  await loop;
102212
102674
  try {
102213
102675
  await pullOnce();
@@ -102223,13 +102685,13 @@ async function recordFrames(source, path, deps = realDeps) {
102223
102685
  lines.push(`file '${frame.file}'`, `duration ${Math.max(seconds, 0.001).toFixed(3)}`);
102224
102686
  }
102225
102687
  lines.push(`file '${frames[frames.length - 1]?.file}'`);
102226
- const list = join13(dir, "frames.txt");
102688
+ const list = join14(dir, "frames.txt");
102227
102689
  await Bun.write(list, `${lines.join(`
102228
102690
  `)}
102229
102691
  `);
102230
102692
  await deps.encode(ffmpeg, list, path);
102231
102693
  } finally {
102232
- await rm5(dir, { recursive: true, force: true }).catch(() => {
102694
+ await rm6(dir, { recursive: true, force: true }).catch(() => {
102233
102695
  return;
102234
102696
  });
102235
102697
  }
@@ -102565,16 +103027,16 @@ async function startPhonePreview(send) {
102565
103027
  }
102566
103028
 
102567
103029
  // src/domains/devices/ios-direct-lane.ts
102568
- import { mkdir as mkdir3, readFile as readFile3 } from "fs/promises";
102569
- import { tmpdir as tmpdir6 } from "os";
102570
- import { dirname as dirname4, join as join16 } from "path";
103030
+ import { mkdir as mkdir3, readFile as readFile4 } from "fs/promises";
103031
+ import { tmpdir as tmpdir7 } from "os";
103032
+ import { dirname as dirname4, join as join17 } from "path";
102571
103033
 
102572
103034
  // src/domains/devices/idb-companion.ts
102573
103035
  import { existsSync as existsSync3 } from "fs";
102574
- import { homedir as homedir8 } from "os";
102575
- import { join as join14 } from "path";
102576
- var IDB_COMPANION_HOME = join14(homedir8(), ".yoqa", "idb", "idb_companion");
102577
- var IDB_CLIENT_HOME = join14(homedir8(), ".yoqa", "idb", "venv", "bin", "idb");
103036
+ import { homedir as homedir9 } from "os";
103037
+ import { join as join15 } from "path";
103038
+ var IDB_COMPANION_HOME = join15(homedir9(), ".yoqa", "idb", "idb_companion");
103039
+ var IDB_CLIENT_HOME = join15(homedir9(), ".yoqa", "idb", "venv", "bin", "idb");
102578
103040
  var MISSING_IDB_MESSAGE = "idb_companion not found. Install the official facebook/idb v1.6.5 macOS binary from GitHub releases to ~/.yoqa/idb/idb_companion (or set YOQA_IDB_COMPANION). Do not use `brew trust facebook/fb`.";
102579
103041
  function resolveIdbCompanionPath(env = process.env, exists2 = existsSync3, which2 = (name30) => Bun.which(name30)) {
102580
103042
  const fromEnv = env.YOQA_IDB_COMPANION?.trim();
@@ -102756,9 +103218,83 @@ function cleanIdbCompleteSource(raw, window2) {
102756
103218
  }
102757
103219
  return { elements, window: window2 };
102758
103220
  }
103221
+ var WEB_STRUCTURAL_ROLES = new Set(["RootWebArea", "InlineTextBox", "LineBreak"]);
103222
+ var WEB_CONTROL_ROLES = new Set([
103223
+ "button",
103224
+ "link",
103225
+ "checkbox",
103226
+ "radio",
103227
+ "switch",
103228
+ "tab",
103229
+ "menuitem",
103230
+ "menuitemcheckbox",
103231
+ "menuitemradio",
103232
+ "option",
103233
+ "textbox",
103234
+ "searchbox",
103235
+ "combobox",
103236
+ "slider",
103237
+ "spinbutton"
103238
+ ]);
103239
+ var WEB_TEXT_ROLES = new Set(["StaticText"]);
103240
+ var GENERATED_DOM_ID = /^(:r[0-9a-z]+:|\u00ABr[0-9a-z]+\u00BB)$/i;
103241
+ function axText(value) {
103242
+ return typeof value?.value === "string" ? value.value.replace(/\s+/g, " ").trim() : "";
103243
+ }
103244
+ function webIdOf(node) {
103245
+ const testId = node.testId?.trim();
103246
+ if (testId)
103247
+ return testId;
103248
+ const domId = node.domId?.trim() ?? "";
103249
+ return GENERATED_DOM_ID.test(domId) ? "" : domId;
103250
+ }
103251
+ function cleanWebAxSource(source, window2) {
103252
+ const byId = new Map(source.axTree.map((node) => [node.nodeId, node]));
103253
+ const namedParentLabel = (node) => {
103254
+ let parent = node.parentId ? byId.get(node.parentId) : undefined;
103255
+ while (parent) {
103256
+ const label = axText(parent.name);
103257
+ if (label && !parent.ignored)
103258
+ return label;
103259
+ parent = parent.parentId ? byId.get(parent.parentId) : undefined;
103260
+ }
103261
+ return "";
103262
+ };
103263
+ const elements = [];
103264
+ for (const node of source.axTree) {
103265
+ if (node.ignored || !node.bounds)
103266
+ continue;
103267
+ if (node.bounds.width <= 0 || node.bounds.height <= 0)
103268
+ continue;
103269
+ const type = axText(node.role);
103270
+ if (WEB_STRUCTURAL_ROLES.has(type))
103271
+ continue;
103272
+ const label = axText(node.name) || axText(node.value);
103273
+ if (!label && !WEB_CONTROL_ROLES.has(type))
103274
+ continue;
103275
+ if (WEB_TEXT_ROLES.has(type) && namedParentLabel(node).includes(label))
103276
+ continue;
103277
+ const id = webIdOf(node);
103278
+ const disabled = node.properties?.some((property) => property.name === "disabled" && property.value?.value === true);
103279
+ pushNormalized(elements, window2, {
103280
+ type,
103281
+ label,
103282
+ id: id || undefined,
103283
+ rect: node.bounds,
103284
+ enabled: !disabled
103285
+ });
103286
+ }
103287
+ return { elements, window: window2 };
103288
+ }
103289
+ function isWebAxSource(parsed) {
103290
+ return typeof parsed === "object" && parsed !== null && Array.isArray(parsed.axTree);
103291
+ }
102759
103292
  function cleanPageSource(xml, window2) {
102760
103293
  const trimmed = xml.trim();
102761
103294
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
103295
+ const parsed = JSON.parse(trimmed);
103296
+ if (isWebAxSource(parsed))
103297
+ return cleanWebAxSource(parsed, window2);
102762
103298
  return cleanIdbCompleteSource(trimmed, window2);
102763
103299
  }
102764
103300
  const elements = [];
@@ -102802,7 +103338,7 @@ function cleanPageSource(xml, window2) {
102802
103338
 
102803
103339
  // src/domains/devices/yoqa-sim.ts
102804
103340
  import { existsSync as existsSync4 } from "fs";
102805
- import { join as join15 } from "path";
103341
+ import { join as join16 } from "path";
102806
103342
 
102807
103343
  class YoqaSimUnreachableError extends Error {
102808
103344
  constructor(message) {
@@ -102813,19 +103349,19 @@ class YoqaSimUnreachableError extends Error {
102813
103349
  function spawnProcess(command) {
102814
103350
  return Bun.spawn(command, { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
102815
103351
  }
102816
- var READY_TIMEOUT_MS2 = 1e4;
103352
+ var READY_TIMEOUT_MS3 = 1e4;
102817
103353
  var STREAM_READY_TIMEOUT_MS = 500;
102818
103354
  var REQUEST_TIMEOUT_MS = 1e4;
102819
- var STOP_GRACE_MS = 2000;
103355
+ var STOP_GRACE_MS2 = 2000;
102820
103356
  var API_READY_RE = /^api_ready (http:\/\/127\.0\.0\.1:\d+)\s*$/;
102821
103357
  var STREAM_READY_RE = /^stream_ready (http:\/\/127\.0\.0\.1:\d+\/\S+)\s*$/;
102822
103358
  function resolveYoqaSimBin() {
102823
103359
  const fromEnv = process.env.YOQA_SIM_BIN?.trim();
102824
103360
  if (fromEnv)
102825
103361
  return fromEnv;
102826
- const root = join15(import.meta.dir, "../../../../../native/yoqa-sim/.build");
103362
+ const root = join16(import.meta.dir, "../../../../../native/yoqa-sim/.build");
102827
103363
  for (const config2 of ["release", "debug"]) {
102828
- const built = join15(root, config2, "yoqa-sim");
103364
+ const built = join16(root, config2, "yoqa-sim");
102829
103365
  if (existsSync4(built))
102830
103366
  return built;
102831
103367
  }
@@ -102885,12 +103421,12 @@ async function spawnYoqaSim(udid, options) {
102885
103421
  proc.kill("SIGTERM");
102886
103422
  const exited = await Promise.race([
102887
103423
  proc.exited.then(() => true),
102888
- Bun.sleep(STOP_GRACE_MS).then(() => false)
103424
+ Bun.sleep(STOP_GRACE_MS2).then(() => false)
102889
103425
  ]);
102890
103426
  if (!exited)
102891
103427
  proc.kill("SIGKILL");
102892
103428
  };
102893
- const timeoutMs = options.readyTimeoutMs ?? READY_TIMEOUT_MS2;
103429
+ const timeoutMs = options.readyTimeoutMs ?? READY_TIMEOUT_MS3;
102894
103430
  let timer;
102895
103431
  const announced = await Promise.race([
102896
103432
  readAnnouncements(proc.stdout, options.streamReadyTimeoutMs ?? STREAM_READY_TIMEOUT_MS, () => clearTimeout(timer)),
@@ -102899,7 +103435,7 @@ async function spawnYoqaSim(udid, options) {
102899
103435
  })
102900
103436
  ]).finally(() => clearTimeout(timer));
102901
103437
  const stderrTail = async () => {
102902
- const text3 = await Promise.race([stderr, Bun.sleep(STOP_GRACE_MS).then(() => "")]);
103438
+ const text3 = await Promise.race([stderr, Bun.sleep(STOP_GRACE_MS2).then(() => "")]);
102903
103439
  return text3.trim().split(`
102904
103440
  `).slice(-3).join(" ");
102905
103441
  };
@@ -102969,6 +103505,23 @@ async function spawnYoqaSim(udid, options) {
102969
103505
  var IOS_ACCEPT_LABELS = ["Allow While Using App", "Allow Once", "Allow", "OK"];
102970
103506
  var IOS_DISMISS_LABELS = ["Don't Allow", "Don\u2019t Allow", "Don't allow"];
102971
103507
  var graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
103508
+ var IOS_HID_KEYCODES = {
103509
+ Enter: 40,
103510
+ Escape: 41,
103511
+ Backspace: 42,
103512
+ Tab: 43,
103513
+ Space: 44,
103514
+ Home: 74,
103515
+ PageUp: 75,
103516
+ Delete: 76,
103517
+ End: 77,
103518
+ PageDown: 78,
103519
+ ArrowRight: 79,
103520
+ ArrowLeft: 80,
103521
+ ArrowDown: 81,
103522
+ ArrowUp: 82
103523
+ };
103524
+ var IOS_SIMULATOR_KEY_SUPPORT = { keys: KEY_NAMES, modifiers: false };
102972
103525
  var IDB_LOST_RE = /StatusCode\.UNAVAILABLE|failed to connect|Connection refused|companion .*(?:not running|terminated)|(?:device|simulator|target) .*not found|not booted/i;
102973
103526
  function idbLostDevice(result) {
102974
103527
  return result.exitCode === 127 || result.exitCode !== 0 && IDB_LOST_RE.test(`${result.stderr}
@@ -103061,7 +103614,7 @@ async function recordViaSimctl(udid, path) {
103061
103614
  };
103062
103615
  }
103063
103616
  async function captureViaSimctl(udid) {
103064
- const dest = join16(tmpdir6(), `yoqa-simctl-${crypto.randomUUID()}.png`);
103617
+ const dest = join17(tmpdir7(), `yoqa-simctl-${crypto.randomUUID()}.png`);
103065
103618
  const proc = Bun.spawn(["xcrun", "simctl", "io", udid, "screenshot", dest], {
103066
103619
  stdout: "pipe",
103067
103620
  stderr: "pipe"
@@ -103070,7 +103623,7 @@ async function captureViaSimctl(udid) {
103070
103623
  if (exitCode !== 0) {
103071
103624
  throw new Error(`simctl screenshot: ${stderr.trim() || `exit ${exitCode}`}`);
103072
103625
  }
103073
- return new Uint8Array(await readFile3(dest));
103626
+ return new Uint8Array(await readFile4(dest));
103074
103627
  }
103075
103628
  async function createIosDirectSession(options, deps = {}) {
103076
103629
  if (options.platform !== "ios") {
@@ -103108,17 +103661,17 @@ async function createIosDirectSession(options, deps = {}) {
103108
103661
  return yoqaSim;
103109
103662
  };
103110
103663
  const viaYoqaSim = async (use, retryable = false) => {
103111
- const running = await startHelpers();
103112
- if (!running)
103664
+ const running2 = await startHelpers();
103665
+ if (!running2)
103113
103666
  return null;
103114
103667
  try {
103115
- return { value: await use(running) };
103668
+ return { value: await use(running2) };
103116
103669
  } catch (error40) {
103117
103670
  if (!retryable && !(error40 instanceof YoqaSimUnreachableError))
103118
103671
  throw error40;
103119
103672
  yoqaSimFailed(error40);
103120
103673
  yoqaSim = Promise.resolve(null);
103121
- await running.stop().catch(() => {
103674
+ await running2.stop().catch(() => {
103122
103675
  return;
103123
103676
  });
103124
103677
  return null;
@@ -103144,11 +103697,11 @@ async function createIosDirectSession(options, deps = {}) {
103144
103697
  return lock.withLock(fn);
103145
103698
  };
103146
103699
  const captureViaIdb = async () => {
103147
- const dest = join16(tmpdir6(), `yoqa-idb-${crypto.randomUUID()}.png`);
103700
+ const dest = join17(tmpdir7(), `yoqa-idb-${crypto.randomUUID()}.png`);
103148
103701
  let bytes;
103149
103702
  try {
103150
103703
  const result = await run(["screenshot", dest, "--udid", udid], "idb screenshot");
103151
- bytes = result.stdoutBytes && result.stdoutBytes.byteLength > 8 ? result.stdoutBytes : new Uint8Array(await readFile3(dest));
103704
+ bytes = result.stdoutBytes && result.stdoutBytes.byteLength > 8 ? result.stdoutBytes : new Uint8Array(await readFile4(dest));
103152
103705
  if (bytes.byteLength === 0)
103153
103706
  throw new Error("idb screenshot returned an empty frame");
103154
103707
  } catch (error40) {
@@ -103161,8 +103714,8 @@ async function createIosDirectSession(options, deps = {}) {
103161
103714
  return bytes;
103162
103715
  };
103163
103716
  const captureFrame = async () => {
103164
- const fromYoqaSim = await viaYoqaSim(async (running) => {
103165
- const frame = await running.frame();
103717
+ const fromYoqaSim = await viaYoqaSim(async (running2) => {
103718
+ const frame = await running2.frame();
103166
103719
  if (frame.bytes.byteLength === 0)
103167
103720
  throw new Error("empty frame");
103168
103721
  return frame;
@@ -103175,7 +103728,7 @@ async function createIosDirectSession(options, deps = {}) {
103175
103728
  const screenshot = async () => {
103176
103729
  await mkdir3(SCREENSHOT_DIR, { recursive: true });
103177
103730
  const frame = await captureFrame();
103178
- const path = join16(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
103731
+ const path = join17(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
103179
103732
  await Bun.write(path, Uint8Array.from(Buffer.from(frame.base64, "base64")));
103180
103733
  return { path, base64: frame.base64 };
103181
103734
  };
@@ -103203,7 +103756,7 @@ async function createIosDirectSession(options, deps = {}) {
103203
103756
  const treeViaYoqaAx = async () => {
103204
103757
  if (!await yoqaAx)
103205
103758
  return null;
103206
- const hash = (await viaYoqaSim((running) => running.frame(), true))?.value.hash;
103759
+ const hash = (await viaYoqaSim((running2) => running2.frame(), true))?.value.hash;
103207
103760
  if (hash && cachedTree?.hash === hash)
103208
103761
  return cachedTree.source;
103209
103762
  const read2 = await viaYoqaAx(async (ax) => {
@@ -103231,14 +103784,14 @@ async function createIosDirectSession(options, deps = {}) {
103231
103784
  await run(["ui", "tap", String(x), String(y), "--udid", udid, "--api", "hid"], "idb ui tap");
103232
103785
  };
103233
103786
  const tapNorm = async (xNorm, yNorm, tapOptions) => {
103234
- const sent = await viaYoqaSim((running) => running.tap(fraction(xNorm), fraction(yNorm), tapOptions?.durationMs));
103787
+ const sent = await viaYoqaSim((running2) => running2.tap(fraction(xNorm), fraction(yNorm), tapOptions?.durationMs));
103235
103788
  if (sent)
103236
103789
  return;
103237
103790
  const size = await getWindowSize();
103238
103791
  await tapPx(toPx(xNorm, size.width), toPx(yNorm, size.height));
103239
103792
  };
103240
103793
  const swipeNorm = async (x1, y1, x2, y2, durationMs) => {
103241
- const sent = await viaYoqaSim((running) => running.swipe({ x: fraction(x1), y: fraction(y1) }, { x: fraction(x2), y: fraction(y2) }, durationMs));
103794
+ const sent = await viaYoqaSim((running2) => running2.swipe({ x: fraction(x1), y: fraction(y1) }, { x: fraction(x2), y: fraction(y2) }, durationMs));
103242
103795
  if (sent)
103243
103796
  return;
103244
103797
  const size = await getWindowSize();
@@ -103266,6 +103819,12 @@ async function createIosDirectSession(options, deps = {}) {
103266
103819
  }
103267
103820
  });
103268
103821
  };
103822
+ const key = async (name30, modifiers = []) => {
103823
+ assertKeySupported(IOS_SIMULATOR_KEY_SUPPORT, "on an iOS simulator", name30, modifiers);
103824
+ await withActionLock(async () => {
103825
+ await run(["ui", "key", String(IOS_HID_KEYCODES[name30]), "--udid", udid], "idb ui key");
103826
+ });
103827
+ };
103269
103828
  const activateApp = async (appId) => {
103270
103829
  await withActionLock(async () => {
103271
103830
  lastAppId = appId;
@@ -103279,7 +103838,7 @@ async function createIosDirectSession(options, deps = {}) {
103279
103838
  };
103280
103839
  const backgroundApp = async (seconds = 3) => {
103281
103840
  await withActionLock(async () => {
103282
- const pressed = await viaYoqaSim((running) => running.key("home"));
103841
+ const pressed = await viaYoqaSim((running2) => running2.key("home"));
103283
103842
  if (!pressed)
103284
103843
  await run(["ui", "button", "HOME", "--udid", udid], "idb HOME");
103285
103844
  await Bun.sleep(Math.max(0, seconds) * 1000);
@@ -103314,10 +103873,10 @@ async function createIosDirectSession(options, deps = {}) {
103314
103873
  quit: async () => {
103315
103874
  quitting = true;
103316
103875
  session.stream = null;
103317
- const [running, ax] = await Promise.all([yoqaSim, yoqaAx]);
103876
+ const [running2, ax] = await Promise.all([yoqaSim, yoqaAx]);
103318
103877
  yoqaSim = null;
103319
103878
  yoqaAx = null;
103320
- await Promise.all([running?.stop(), ax?.stop()]);
103879
+ await Promise.all([running2?.stop(), ax?.stop()]);
103321
103880
  },
103322
103881
  startRecording: (path) => recordViaSimctl(udid, path),
103323
103882
  captureFrame,
@@ -103328,6 +103887,8 @@ async function createIosDirectSession(options, deps = {}) {
103328
103887
  swipe,
103329
103888
  drag: (x1, y1, x2, y2, durationMs = 800) => swipe(x1, y1, x2, y2, durationMs),
103330
103889
  type,
103890
+ keySupport: IOS_SIMULATOR_KEY_SUPPORT,
103891
+ key,
103331
103892
  activateApp,
103332
103893
  setTargetApp: (appId) => {
103333
103894
  lastAppId = appId;
@@ -103387,10 +103948,10 @@ function yoqaAxTreeToSource(tree, window2) {
103387
103948
  }));
103388
103949
  return JSON.stringify(tree.degraded ? { elements, degraded: true } : { elements });
103389
103950
  }
103390
- function liveStream(running) {
103391
- if (!running?.streamUrl)
103951
+ function liveStream(running2) {
103952
+ if (!running2?.streamUrl)
103392
103953
  return null;
103393
- const { streamUrl } = running;
103954
+ const { streamUrl } = running2;
103394
103955
  return { ready: true, port: Number(new URL(streamUrl).port), upstreamUrl: streamUrl };
103395
103956
  }
103396
103957
 
@@ -103615,10 +104176,10 @@ async function connectUsbmux(udid, port, socketPath = USBMUXD_SOCKET) {
103615
104176
 
103616
104177
  // src/domains/devices/yoqa-runner.ts
103617
104178
  import { X509Certificate, createHash } from "crypto";
103618
- import { existsSync as existsSync5 } from "fs";
103619
- import { copyFile, cp, mkdir as mkdir4, readFile as readFile4, readdir as readdir2, rename, rm as rm6, writeFile as writeFile4 } from "fs/promises";
103620
- import { homedir as homedir9 } from "os";
103621
- import { dirname as dirname5, join as join17, relative } from "path";
104179
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync, renameSync, rmSync } from "fs";
104180
+ import { copyFile, cp, mkdir as mkdir4, readFile as readFile5, readdir as readdir2, rename, rm as rm7, writeFile as writeFile4 } from "fs/promises";
104181
+ import { homedir as homedir10 } from "os";
104182
+ import { dirname as dirname5, join as join18, relative } from "path";
103622
104183
 
103623
104184
  class YoqaRunnerError extends Error {
103624
104185
  constructor(message) {
@@ -103680,7 +104241,7 @@ function normalizeTeamId(value) {
103680
104241
  }
103681
104242
  async function readSavedTeam(settingsPath) {
103682
104243
  try {
103683
- const parsed = JSON.parse(await readFile4(settingsPath, "utf8"));
104244
+ const parsed = JSON.parse(await readFile5(settingsPath, "utf8"));
103684
104245
  return typeof parsed.team === "string" ? normalizeTeamId(parsed.team) : null;
103685
104246
  } catch {
103686
104247
  return null;
@@ -103688,7 +104249,7 @@ async function readSavedTeam(settingsPath) {
103688
104249
  }
103689
104250
  async function saveTeam(settingsPath, team) {
103690
104251
  if (team === null) {
103691
- await rm6(settingsPath, { force: true });
104252
+ await rm7(settingsPath, { force: true });
103692
104253
  return;
103693
104254
  }
103694
104255
  await mkdir4(dirname5(settingsPath), { recursive: true });
@@ -103757,16 +104318,16 @@ async function hashSources(projectDir) {
103757
104318
  entries = (await readdir2(path)).sort();
103758
104319
  } catch {
103759
104320
  hash.update(`${relative(projectDir, path)}\x00`);
103760
- hash.update(await readFile4(path));
104321
+ hash.update(await readFile5(path));
103761
104322
  return;
103762
104323
  }
103763
104324
  for (const entry of entries) {
103764
104325
  if (!IGNORED.has(entry))
103765
- await visit9(join17(path, entry));
104326
+ await visit9(join18(path, entry));
103766
104327
  }
103767
104328
  };
103768
104329
  for (const entry of SOURCE_ENTRIES) {
103769
- const path = join17(projectDir, entry);
104330
+ const path = join18(projectDir, entry);
103770
104331
  if (existsSync5(path))
103771
104332
  await visit9(path);
103772
104333
  }
@@ -103787,11 +104348,11 @@ function parseProfile(plist) {
103787
104348
  };
103788
104349
  }
103789
104350
  async function readProfiles(products, exec) {
103790
- const platform = join17(products, "Debug-iphoneos");
104351
+ const platform = join18(products, "Debug-iphoneos");
103791
104352
  const apps2 = (await readdir2(platform).catch(() => [])).filter((name30) => name30.endsWith(".app"));
103792
104353
  const profiles = [];
103793
104354
  for (const app of apps2) {
103794
- const path = join17(platform, app, "embedded.mobileprovision");
104355
+ const path = join18(platform, app, "embedded.mobileprovision");
103795
104356
  if (!existsSync5(path))
103796
104357
  continue;
103797
104358
  const decoded = await exec(["security", "cms", "-D", "-i", path]);
@@ -103806,7 +104367,7 @@ function usableFor(manifest, udid, now) {
103806
104367
  }
103807
104368
  async function readManifest(path) {
103808
104369
  try {
103809
- return JSON.parse(await readFile4(path, "utf8"));
104370
+ return JSON.parse(await readFile5(path, "utf8"));
103810
104371
  } catch {
103811
104372
  return null;
103812
104373
  }
@@ -103814,14 +104375,14 @@ async function readManifest(path) {
103814
104375
  async function findXctestrun(products) {
103815
104376
  const entries = await readdir2(products).catch(() => []);
103816
104377
  const name30 = entries.find((entry) => entry.endsWith(".xctestrun"));
103817
- return name30 ? join17(products, name30) : null;
104378
+ return name30 ? join18(products, name30) : null;
103818
104379
  }
103819
104380
  async function brandRunnerApp(products, exec) {
103820
- const platform = join17(products, "Debug-iphoneos");
103821
- const host = join17(platform, "YoqaRunner.app");
103822
- const runner = join17(platform, "YoqaRunnerUITests-Runner.app");
103823
- const staging = join17(products, "branding");
103824
- const copy = join17(staging, "YoqaRunnerUITests-Runner.app");
104381
+ const platform = join18(products, "Debug-iphoneos");
104382
+ const host = join18(platform, "YoqaRunner.app");
104383
+ const runner = join18(platform, "YoqaRunnerUITests-Runner.app");
104384
+ const staging = join18(products, "branding");
104385
+ const copy = join18(staging, "YoqaRunnerUITests-Runner.app");
103825
104386
  const run = async (command) => {
103826
104387
  const result = await exec(command);
103827
104388
  if (result.exitCode !== 0) {
@@ -103833,10 +104394,10 @@ async function brandRunnerApp(products, exec) {
103833
104394
  const icons = (await readdir2(host)).filter((name30) => name30.startsWith("AppIcon") || name30 === "Assets.car");
103834
104395
  if (!icons.some((name30) => name30.startsWith("AppIcon")))
103835
104396
  return "the host app has no AppIcon";
103836
- await rm6(staging, { recursive: true, force: true });
104397
+ await rm7(staging, { recursive: true, force: true });
103837
104398
  await cp(runner, copy, { recursive: true, verbatimSymlinks: true });
103838
104399
  for (const name30 of icons)
103839
- await copyFile(join17(host, name30), join17(copy, name30));
104400
+ await copyFile(join18(host, name30), join18(copy, name30));
103840
104401
  const plist = await run([
103841
104402
  "plutil",
103842
104403
  "-extract",
@@ -103844,7 +104405,7 @@ async function brandRunnerApp(products, exec) {
103844
104405
  "json",
103845
104406
  "-o",
103846
104407
  "-",
103847
- join17(host, "Info.plist")
104408
+ join18(host, "Info.plist")
103848
104409
  ]);
103849
104410
  await run([
103850
104411
  "plutil",
@@ -103852,11 +104413,11 @@ async function brandRunnerApp(products, exec) {
103852
104413
  "CFBundleIcons",
103853
104414
  "-json",
103854
104415
  plist.trim(),
103855
- join17(copy, "Info.plist")
104416
+ join18(copy, "Info.plist")
103856
104417
  ]);
103857
- const certificates = join17(staging, "certificate");
104418
+ const certificates = join18(staging, "certificate");
103858
104419
  await run(["codesign", "-d", `--extract-certificates=${certificates}`, runner]);
103859
- const identity2 = createHash("sha1").update(await readFile4(`${certificates}0`)).digest("hex").toUpperCase();
104420
+ const identity2 = createHash("sha1").update(await readFile5(`${certificates}0`)).digest("hex").toUpperCase();
103860
104421
  await run([
103861
104422
  "codesign",
103862
104423
  "--force",
@@ -103866,7 +104427,7 @@ async function brandRunnerApp(products, exec) {
103866
104427
  "--generate-entitlement-der",
103867
104428
  copy
103868
104429
  ]);
103869
- const unbranded = join17(staging, "unbranded.app");
104430
+ const unbranded = join18(staging, "unbranded.app");
103870
104431
  await rename(runner, unbranded);
103871
104432
  try {
103872
104433
  await rename(copy, runner);
@@ -103878,7 +104439,7 @@ async function brandRunnerApp(products, exec) {
103878
104439
  } catch (error40) {
103879
104440
  return error40 instanceof Error ? error40.message : String(error40);
103880
104441
  } finally {
103881
- await rm6(staging, { recursive: true, force: true });
104442
+ await rm7(staging, { recursive: true, force: true });
103882
104443
  }
103883
104444
  }
103884
104445
  function xcodeVersion(result) {
@@ -103938,14 +104499,17 @@ function prepareYoqaRunner(udid, deps) {
103938
104499
  return next;
103939
104500
  }
103940
104501
  async function prepare(udid, deps) {
104502
+ if (!existsSync5(join18(deps.projectDir, "YoqaRunner.xcodeproj"))) {
104503
+ throw new YoqaRunnerError(`YoqaRunner's Xcode project is missing from this install (looked in ${deps.projectDir}). Reinstall yoqa, or set YOQA_RUNNER_PROJECT to a native/yoqa-runner checkout.`);
104504
+ }
103941
104505
  const team = await signingTeam(deps);
103942
104506
  const [sources, xcode] = await Promise.all([
103943
104507
  hashSources(deps.projectDir),
103944
104508
  deps.exec(["xcodebuild", "-version"])
103945
104509
  ]);
103946
104510
  const key = createHash("sha256").update(`${sources}\x00${xcodeVersion(xcode)}\x00${team}`).digest("hex").slice(0, 16);
103947
- const dir = join17(deps.cacheRoot, key);
103948
- const manifestPath = join17(dir, "build.json");
104511
+ const dir = join18(deps.cacheRoot, key);
104512
+ const manifestPath = join18(dir, "build.json");
103949
104513
  const cached2 = await readManifest(manifestPath);
103950
104514
  if (cached2 && usableFor(cached2, udid, deps.now())) {
103951
104515
  return {
@@ -103956,12 +104520,12 @@ async function prepare(udid, deps) {
103956
104520
  devices: cached2.devices
103957
104521
  };
103958
104522
  }
103959
- const derivedData = join17(dir, "DerivedData");
104523
+ const derivedData = join18(dir, "DerivedData");
103960
104524
  const build = await deps.exec([
103961
104525
  "xcodebuild",
103962
104526
  "build-for-testing",
103963
104527
  "-project",
103964
- join17(deps.projectDir, "YoqaRunner.xcodeproj"),
104528
+ join18(deps.projectDir, "YoqaRunner.xcodeproj"),
103965
104529
  "-scheme",
103966
104530
  "YoqaRunner",
103967
104531
  "-destination",
@@ -103978,7 +104542,7 @@ async function prepare(udid, deps) {
103978
104542
  throw new YoqaRunnerError(explainBuildFailure(`${build.stdout}
103979
104543
  ${build.stderr}`, team));
103980
104544
  }
103981
- const products = join17(derivedData, "Build/Products");
104545
+ const products = join18(derivedData, "Build/Products");
103982
104546
  const unbranded = await brandRunnerApp(products, deps.exec);
103983
104547
  if (unbranded)
103984
104548
  console.warn(`[yoqa-runner] the runner keeps Xcode's blank icon: ${unbranded}`);
@@ -104005,7 +104569,7 @@ ${build.stderr}`, team));
104005
104569
  devices: manifest.devices
104006
104570
  };
104007
104571
  }
104008
- var READY_TIMEOUT_MS3 = 120000;
104572
+ var READY_TIMEOUT_MS4 = 120000;
104009
104573
  var LISTENING_RE = /YOQA_RUNNER_LISTENING port=(\d+)/;
104010
104574
  var LAUNCH_FAILED_RE = /IDELaunchReport: .*Finished with error:|Failed to install or launch the test runner/;
104011
104575
  var OUTPUT_TAIL_CHARS = 16000;
@@ -104035,7 +104599,7 @@ function drain(stream, onLine) {
104035
104599
  }
104036
104600
  async function startYoqaRunner(udid, deps) {
104037
104601
  const build = await prepareYoqaRunner(udid, deps);
104038
- const readyTimeoutMs = deps.readyTimeoutMs ?? READY_TIMEOUT_MS3;
104602
+ const readyTimeoutMs = deps.readyTimeoutMs ?? READY_TIMEOUT_MS4;
104039
104603
  const env = {};
104040
104604
  const port = deps.env.YOQA_RUNNER_PORT?.trim();
104041
104605
  if (port)
@@ -104128,7 +104692,36 @@ async function execCommand(command) {
104128
104692
  return { stdout: "", stderr: `failed to spawn: ${command[0]}`, exitCode: 127 };
104129
104693
  }
104130
104694
  }
104695
+ var RUNNER_SOURCE_ARCHIVE = "yoqa-runner-src.tar.gz";
104696
+ function resolveYoqaRunnerProject(at) {
104697
+ const fromEnv = at.env.YOQA_RUNNER_PROJECT?.trim();
104698
+ if (fromEnv)
104699
+ return fromEnv;
104700
+ if (existsSync5(join18(at.checkout, "YoqaRunner.xcodeproj")))
104701
+ return at.checkout;
104702
+ if (!existsSync5(at.archive))
104703
+ return at.checkout;
104704
+ const key = createHash("sha256").update(readFileSync(at.archive)).digest("hex").slice(0, 16);
104705
+ const dir = join18(at.cacheRoot, "source", key);
104706
+ if (existsSync5(join18(dir, "YoqaRunner.xcodeproj")))
104707
+ return dir;
104708
+ const staging = `${dir}.${process.pid}.tmp`;
104709
+ rmSync(staging, { recursive: true, force: true });
104710
+ mkdirSync2(staging, { recursive: true });
104711
+ const tar = Bun.spawnSync(["tar", "-xzf", at.archive, "-C", staging]);
104712
+ if (tar.exitCode !== 0) {
104713
+ rmSync(staging, { recursive: true, force: true });
104714
+ return at.checkout;
104715
+ }
104716
+ try {
104717
+ renameSync(staging, dir);
104718
+ } catch {
104719
+ rmSync(staging, { recursive: true, force: true });
104720
+ }
104721
+ return dir;
104722
+ }
104131
104723
  function yoqaRunnerDeps() {
104724
+ const cacheRoot = join18(homedir10(), ".yoqa", "yoqa-runner");
104132
104725
  return {
104133
104726
  exec: execCommand,
104134
104727
  spawn: (command, env) => Bun.spawn(command, {
@@ -104138,9 +104731,14 @@ function yoqaRunnerDeps() {
104138
104731
  stderr: "pipe"
104139
104732
  }),
104140
104733
  env: process.env,
104141
- projectDir: join17(import.meta.dir, "../../../../../native/yoqa-runner"),
104142
- cacheRoot: join17(homedir9(), ".yoqa", "yoqa-runner"),
104143
- settingsPath: join17(homedir9(), ".yoqa", "ios-signing.json"),
104734
+ projectDir: resolveYoqaRunnerProject({
104735
+ env: process.env,
104736
+ checkout: join18(import.meta.dir, "../../../../../native/yoqa-runner"),
104737
+ archive: join18(dirname5(process.execPath), RUNNER_SOURCE_ARCHIVE),
104738
+ cacheRoot
104739
+ }),
104740
+ cacheRoot,
104741
+ settingsPath: join18(homedir10(), ".yoqa", "ios-signing.json"),
104144
104742
  now: () => new Date
104145
104743
  };
104146
104744
  }
@@ -104152,6 +104750,15 @@ var SWIPE_MS = 400;
104152
104750
  var DRAG_PRESS_MS = 500;
104153
104751
  var BACKSPACE = String.fromCharCode(8);
104154
104752
  var KEY_SEPARATORS = new RegExp(`(\\r?\\n|${BACKSPACE})`);
104753
+ var PHONE_KEYS = {
104754
+ Enter: ["keyboardReturn", {}],
104755
+ Backspace: ["keyboardDelete", {}],
104756
+ Space: ["type", { text: " " }]
104757
+ };
104758
+ var PHONE_KEY_SUPPORT = {
104759
+ keys: Object.keys(PHONE_KEYS),
104760
+ modifiers: false
104761
+ };
104155
104762
  var RECORDING_PULL_MS = 400;
104156
104763
  var UNREACHABLE_GRACE_MS = 20000;
104157
104764
  function errorMessage5(error40) {
@@ -104375,7 +104982,7 @@ async function createIosDeviceSession(options, deps = iosDeviceDeps()) {
104375
104982
  const screenshot = async () => {
104376
104983
  await mkdir5(SCREENSHOT_DIR, { recursive: true });
104377
104984
  const frame = await captureFrame();
104378
- const path = join18(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
104985
+ const path = join19(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
104379
104986
  await Bun.write(path, Uint8Array.from(Buffer.from(frame.base64, "base64")));
104380
104987
  return { path, base64: frame.base64 };
104381
104988
  };
@@ -104429,6 +105036,12 @@ async function createIosDeviceSession(options, deps = iosDeviceDeps()) {
104429
105036
  swipe: (x1, y1, x2, y2, durationMs = SWIPE_MS) => withActionLock(() => dragNorm({ x: x1, y: y1 }, { x: x2, y: y2 }, durationMs, SWIPE_PRESS_MS)),
104430
105037
  drag: (x1, y1, x2, y2, durationMs = 800) => withActionLock(() => dragNorm({ x: x1, y: y1 }, { x: x2, y: y2 }, durationMs, DRAG_PRESS_MS)),
104431
105038
  type: (text3) => withActionLock(() => typeText(text3)),
105039
+ keySupport: PHONE_KEY_SUPPORT,
105040
+ key: async (name30, modifiers = []) => {
105041
+ assertKeySupported(PHONE_KEY_SUPPORT, "on a cabled iPhone", name30, modifiers);
105042
+ const [command, fields] = PHONE_KEYS[name30];
105043
+ await withActionLock(() => call(command, fields, true));
105044
+ },
104432
105045
  activateApp: async (id) => {
104433
105046
  await devicectl(["device", "process", "launch", "--device", udid, id], "devicectl launch");
104434
105047
  appId = id;
@@ -104494,21 +105107,645 @@ function directImplementationOrder(implementations, optIn) {
104494
105107
  var OPT_IN_ENV = {
104495
105108
  android: "YOQA_DIRECT_ANDROID",
104496
105109
  "ios-simulator": "YOQA_DIRECT_IOS_SIMULATOR",
104497
- "ios-device": "YOQA_DIRECT_IOS_DEVICE"
105110
+ "ios-device": "YOQA_DIRECT_IOS_DEVICE",
105111
+ web: "YOQA_DIRECT_WEB"
104498
105112
  };
104499
105113
  function directOptIn(deviceClass, env = process.env) {
104500
105114
  const value = env[OPT_IN_ENV[deviceClass]]?.trim();
104501
105115
  return value ? value : undefined;
104502
105116
  }
104503
105117
 
105118
+ // src/domains/devices/web-direct-lane.ts
105119
+ import { mkdir as mkdir6 } from "fs/promises";
105120
+ import { join as join20 } from "path";
105121
+
105122
+ // src/domains/devices/cdp-client.ts
105123
+ class CdpClosedError extends Error {
105124
+ constructor(message = "The browser's DevTools connection closed") {
105125
+ super(message);
105126
+ this.name = "CdpClosedError";
105127
+ }
105128
+ }
105129
+
105130
+ class CdpCommandError extends Error {
105131
+ method;
105132
+ code;
105133
+ constructor(method, code, message) {
105134
+ super(`${method}: ${message}`);
105135
+ this.method = method;
105136
+ this.code = code;
105137
+ this.name = "CdpCommandError";
105138
+ }
105139
+ }
105140
+ var COMMAND_TIMEOUT_MS2 = 30000;
105141
+ var CONNECT_TIMEOUT_MS = 5000;
105142
+ async function connectCdp(url2, options = {}) {
105143
+ const connectTimeoutMs = options.connectTimeoutMs ?? CONNECT_TIMEOUT_MS;
105144
+ const socket = new WebSocket(url2);
105145
+ await new Promise((resolve7, reject) => {
105146
+ const timer = setTimeout(() => {
105147
+ socket.close();
105148
+ reject(new Error(`No DevTools socket at ${url2} within ${connectTimeoutMs} ms`));
105149
+ }, connectTimeoutMs);
105150
+ socket.addEventListener("open", () => {
105151
+ clearTimeout(timer);
105152
+ resolve7();
105153
+ });
105154
+ socket.addEventListener("error", () => {
105155
+ clearTimeout(timer);
105156
+ reject(new Error(`Could not open the DevTools socket at ${url2}`));
105157
+ });
105158
+ });
105159
+ const commandTimeoutMs = options.commandTimeoutMs ?? COMMAND_TIMEOUT_MS2;
105160
+ const pending = new Map;
105161
+ const listeners = new Map;
105162
+ let nextId = 1;
105163
+ let isClosed = false;
105164
+ let markClosed = () => {
105165
+ return;
105166
+ };
105167
+ const closed = new Promise((resolve7) => {
105168
+ markClosed = resolve7;
105169
+ });
105170
+ const onClosed = () => {
105171
+ if (isClosed)
105172
+ return;
105173
+ isClosed = true;
105174
+ for (const entry of pending.values()) {
105175
+ clearTimeout(entry.timer);
105176
+ entry.reject(new CdpClosedError);
105177
+ }
105178
+ pending.clear();
105179
+ markClosed();
105180
+ };
105181
+ socket.addEventListener("close", onClosed);
105182
+ socket.addEventListener("error", onClosed);
105183
+ socket.addEventListener("message", (event) => {
105184
+ let message;
105185
+ try {
105186
+ message = JSON.parse(String(event.data));
105187
+ } catch {
105188
+ return;
105189
+ }
105190
+ if (message.id !== undefined) {
105191
+ const entry = pending.get(message.id);
105192
+ if (!entry)
105193
+ return;
105194
+ pending.delete(message.id);
105195
+ clearTimeout(entry.timer);
105196
+ if (message.error) {
105197
+ entry.reject(new CdpCommandError(entry.method, message.error.code, message.error.message ?? "failed"));
105198
+ } else {
105199
+ entry.resolve(message.result ?? {});
105200
+ }
105201
+ return;
105202
+ }
105203
+ if (message.method) {
105204
+ for (const listener of listeners.get(message.method) ?? []) {
105205
+ listener(message.params, message.sessionId);
105206
+ }
105207
+ }
105208
+ });
105209
+ return {
105210
+ send: (method, params, sessionId) => {
105211
+ if (isClosed)
105212
+ return Promise.reject(new CdpClosedError);
105213
+ const id = nextId++;
105214
+ return new Promise((resolve7, reject) => {
105215
+ const timer = setTimeout(() => {
105216
+ pending.delete(id);
105217
+ reject(new Error(`${method}: the browser did not answer within ${commandTimeoutMs} ms`));
105218
+ }, commandTimeoutMs);
105219
+ pending.set(id, {
105220
+ method,
105221
+ resolve: resolve7,
105222
+ reject,
105223
+ timer
105224
+ });
105225
+ socket.send(JSON.stringify({ id, method, params, ...sessionId ? { sessionId } : {} }));
105226
+ });
105227
+ },
105228
+ on: (method, listener) => {
105229
+ const set2 = listeners.get(method) ?? new Set;
105230
+ set2.add(listener);
105231
+ listeners.set(method, set2);
105232
+ return () => {
105233
+ set2.delete(listener);
105234
+ };
105235
+ },
105236
+ closed,
105237
+ close: async () => {
105238
+ if (isClosed)
105239
+ return;
105240
+ socket.close();
105241
+ await closed;
105242
+ }
105243
+ };
105244
+ }
105245
+
105246
+ // src/domains/devices/chromium-attach.ts
105247
+ var ATTACH_ID = /^chromium-cdp-(\d{1,5})$/;
105248
+ var PROBE_TIMEOUT_MS = 2000;
105249
+ function attachPortOf(deviceId) {
105250
+ const match = ATTACH_ID.exec(deviceId);
105251
+ const port = Number(match?.[1]);
105252
+ return match && port >= 1 && port <= 65535 ? port : null;
105253
+ }
105254
+
105255
+ class NoCdpEndpointError extends Error {
105256
+ port;
105257
+ constructor(port, reason) {
105258
+ super(`No Chromium DevTools endpoint on port ${port} (${reason}). Start Chrome, Edge or Brave with --remote-debugging-port=${port} and a non-default --user-data-dir: Chrome 136 and later turn remote debugging off on the default profile.`);
105259
+ this.port = port;
105260
+ this.name = "NoCdpEndpointError";
105261
+ }
105262
+ }
105263
+ async function browserSocketUrl(port, fetchFn = fetch) {
105264
+ let response;
105265
+ try {
105266
+ response = await fetchFn(`http://127.0.0.1:${port}/json/version`, {
105267
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
105268
+ });
105269
+ } catch (error40) {
105270
+ throw new NoCdpEndpointError(port, error40 instanceof Error ? error40.message : String(error40));
105271
+ }
105272
+ if (!response.ok)
105273
+ throw new NoCdpEndpointError(port, `/json/version answered ${response.status}`);
105274
+ const body = await response.json().catch(() => null);
105275
+ const url2 = body?.webSocketDebuggerUrl;
105276
+ if (typeof url2 !== "string" || !url2.startsWith("ws")) {
105277
+ throw new NoCdpEndpointError(port, "/json/version named no DevTools socket");
105278
+ }
105279
+ return url2;
105280
+ }
105281
+
105282
+ // src/domains/devices/web-direct-lane.ts
105283
+ var NEWLINE = /(\r?\n)/;
105284
+ var DRAG_STEPS = 10;
105285
+ var DEFAULT_DRAG_MS = 800;
105286
+ var DEFAULT_SWIPE_MS = 400;
105287
+ var NAVIGATE_LOAD_MS = 1e4;
105288
+ var MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1";
105289
+ var CDP_KEYS = {
105290
+ Enter: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13, text: "\r" },
105291
+ Tab: { key: "Tab", code: "Tab", windowsVirtualKeyCode: 9 },
105292
+ Escape: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
105293
+ Backspace: { key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 },
105294
+ Delete: { key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 },
105295
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
105296
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
105297
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", windowsVirtualKeyCode: 37 },
105298
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", windowsVirtualKeyCode: 39 },
105299
+ Home: { key: "Home", code: "Home", windowsVirtualKeyCode: 36 },
105300
+ End: { key: "End", code: "End", windowsVirtualKeyCode: 35 },
105301
+ PageUp: { key: "PageUp", code: "PageUp", windowsVirtualKeyCode: 33 },
105302
+ PageDown: { key: "PageDown", code: "PageDown", windowsVirtualKeyCode: 34 },
105303
+ Space: { key: " ", code: "Space", windowsVirtualKeyCode: 32, text: " " }
105304
+ };
105305
+ var CDP_MODIFIERS = {
105306
+ Shift: { key: "Shift", code: "ShiftLeft", windowsVirtualKeyCode: 16, bit: 8 },
105307
+ Control: { key: "Control", code: "ControlLeft", windowsVirtualKeyCode: 17, bit: 2 },
105308
+ Alt: { key: "Alt", code: "AltLeft", windowsVirtualKeyCode: 18, bit: 1 },
105309
+ Meta: { key: "Meta", code: "MetaLeft", windowsVirtualKeyCode: 91, bit: 4 }
105310
+ };
105311
+ var WEB_KEY_SUPPORT = { keys: KEY_NAMES, modifiers: true };
105312
+ function webDirectDeps(overrides = {}) {
105313
+ return {
105314
+ browserSocketUrl: (port) => browserSocketUrl(port),
105315
+ connect: (url2) => connectCdp(url2),
105316
+ installedBrowsers: overrides.installedBrowsers ?? (() => findInstalledBrowsers()),
105317
+ launch: (browser) => launchChromium(browser, { spawn: overrides.spawn })
105318
+ };
105319
+ }
105320
+ async function openBrowser(deviceId, deps) {
105321
+ const port = attachPortOf(deviceId);
105322
+ if (port !== null) {
105323
+ return { socketUrl: await deps.browserSocketUrl(port), owned: null, name: null };
105324
+ }
105325
+ const browserId = installedBrowserIdOf(deviceId);
105326
+ if (browserId === null) {
105327
+ throw new Error("A web device is an installed browser (web-chrome, web-edge, web-brave, web-chromium) or a Chromium started with --remote-debugging-port, named chromium-cdp-<port>");
105328
+ }
105329
+ const browser = (await deps.installedBrowsers()).find((installed) => installed.id === browserId);
105330
+ if (!browser) {
105331
+ throw new Error(`${browserId} is not installed. Run yoqa devices web to list the browsers here`);
105332
+ }
105333
+ const owned = await deps.launch(browser);
105334
+ const name30 = browser.version ? `${browser.name} ${browser.version}` : browser.name;
105335
+ return { socketUrl: owned.socketUrl, owned, name: name30 };
105336
+ }
105337
+ async function pickPage(client) {
105338
+ const { targetInfos } = await client.send("Target.getTargets", {});
105339
+ const page = targetInfos.find((target) => target.type === "page" && !target.url.startsWith("devtools://"));
105340
+ if (page)
105341
+ return page.targetId;
105342
+ return (await client.send("Target.createTarget", { url: "about:blank" })).targetId;
105343
+ }
105344
+ function layoutByBackendId(documents, strings) {
105345
+ const layouts = new Map;
105346
+ const doc2 = documents[0];
105347
+ if (!doc2)
105348
+ return layouts;
105349
+ const scrollX = doc2.scrollOffsetX ?? 0;
105350
+ const scrollY = doc2.scrollOffsetY ?? 0;
105351
+ doc2.layout.nodeIndex.forEach((nodeIndex, layoutIndex) => {
105352
+ const backendId = doc2.nodes.backendNodeId?.[nodeIndex];
105353
+ const [x = 0, y = 0, width = 0, height = 0] = doc2.layout.bounds[layoutIndex] ?? [];
105354
+ if (backendId === undefined || layouts.has(backendId))
105355
+ return;
105356
+ const layout = { bounds: { x: x - scrollX, y: y - scrollY, width, height } };
105357
+ const attributes = doc2.nodes.attributes?.[nodeIndex] ?? [];
105358
+ for (let at = 0;at + 1 < attributes.length; at += 2) {
105359
+ const name30 = strings[attributes[at] ?? -1];
105360
+ const value = strings[attributes[at + 1] ?? -1];
105361
+ if (name30 === "data-testid" && value)
105362
+ layout.testId = value;
105363
+ if (name30 === "id" && value)
105364
+ layout.domId = value;
105365
+ }
105366
+ layouts.set(backendId, layout);
105367
+ });
105368
+ return layouts;
105369
+ }
105370
+ function webUrlOf(appId) {
105371
+ const url2 = URL.canParse(appId) ? new URL(appId) : null;
105372
+ if (url2?.protocol !== "http:" && url2?.protocol !== "https:") {
105373
+ throw new Error(`On web an app is its Web URL (http or https), and "${appId}" is not one`);
105374
+ }
105375
+ return url2;
105376
+ }
105377
+ function originOf(url2) {
105378
+ return URL.canParse(url2) ? new URL(url2).origin : null;
105379
+ }
105380
+ async function attachPage(client, targetId, preset) {
105381
+ const { sessionId } = await client.send("Target.attachToTarget", { targetId, flatten: true });
105382
+ await client.send("Page.enable", {}, sessionId);
105383
+ const { width, height, deviceScaleFactor, mobile } = preset;
105384
+ await client.send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor, mobile }, sessionId);
105385
+ if (mobile) {
105386
+ await client.send("Emulation.setTouchEmulationEnabled", { enabled: true, maxTouchPoints: 5 }, sessionId);
105387
+ await client.send("Emulation.setUserAgentOverride", { userAgent: MOBILE_USER_AGENT }, sessionId);
105388
+ }
105389
+ await client.send("Page.bringToFront", {}, sessionId);
105390
+ return { targetId, sessionId };
105391
+ }
105392
+ async function createWebDirectSession(options, deps = webDirectDeps()) {
105393
+ if (options.platform !== "web") {
105394
+ throw new Error("The web Direct lane drives web devices only");
105395
+ }
105396
+ const preset = VIEWPORT_PRESETS[options.viewport ?? DEFAULT_VIEWPORT_PRESET];
105397
+ const { socketUrl, owned, name: name30 } = await openBrowser(options.deviceId, deps);
105398
+ const client = await deps.connect(socketUrl).catch(async (error40) => {
105399
+ await owned?.stop();
105400
+ throw error40;
105401
+ });
105402
+ let page;
105403
+ let browser;
105404
+ try {
105405
+ page = await attachPage(client, await pickPage(client), preset);
105406
+ await client.send("Target.setDiscoverTargets", { discover: true });
105407
+ browser = name30 ?? (await client.send("Browser.getVersion", {})).product.replace("/", " ");
105408
+ } catch (error40) {
105409
+ await client.close().catch(() => {
105410
+ return;
105411
+ });
105412
+ await owned?.stop();
105413
+ throw error40;
105414
+ }
105415
+ let quitting = false;
105416
+ let sessionEnded = null;
105417
+ const dead = (reason) => {
105418
+ if (!sessionEnded) {
105419
+ sessionEnded = new DeadSessionError(`Device session ended: ${reason}`);
105420
+ if (!quitting) {
105421
+ owned?.stop();
105422
+ options.onSessionDead?.();
105423
+ }
105424
+ }
105425
+ return sessionEnded;
105426
+ };
105427
+ client.closed.then(() => {
105428
+ if (!quitting)
105429
+ dead("the browser closed its DevTools connection");
105430
+ });
105431
+ owned?.exited.then(() => {
105432
+ if (!quitting)
105433
+ dead("the browser quit");
105434
+ });
105435
+ let openDialog = null;
105436
+ const openers = [];
105437
+ let switching = Promise.resolve();
105438
+ const switchPage = (fn) => {
105439
+ switching = switching.then(fn).catch(() => {
105440
+ return;
105441
+ });
105442
+ };
105443
+ client.on("Target.targetCreated", (params) => {
105444
+ const { targetInfo } = params;
105445
+ if (targetInfo.type !== "page" || !targetInfo.openerId)
105446
+ return;
105447
+ switchPage(async () => {
105448
+ if (targetInfo.openerId !== page.targetId || sessionEnded)
105449
+ return;
105450
+ const popup = await attachPage(client, targetInfo.targetId, preset);
105451
+ openers.push(page);
105452
+ page = popup;
105453
+ openDialog = null;
105454
+ });
105455
+ });
105456
+ client.on("Target.detachedFromTarget", (params) => {
105457
+ const { sessionId } = params ?? {};
105458
+ const closedOpener = openers.findIndex((opener2) => opener2.sessionId === sessionId);
105459
+ if (closedOpener >= 0)
105460
+ openers.splice(closedOpener, 1);
105461
+ if (sessionId !== page.sessionId || quitting)
105462
+ return;
105463
+ const opener = openers.pop();
105464
+ if (!opener) {
105465
+ dead("the page was closed");
105466
+ return;
105467
+ }
105468
+ page = opener;
105469
+ openDialog = null;
105470
+ switchPage(() => client.send("Page.bringToFront", {}, opener.sessionId));
105471
+ });
105472
+ client.on("Page.javascriptDialogOpening", (params, eventSession) => {
105473
+ if (eventSession === page.sessionId)
105474
+ openDialog = params;
105475
+ });
105476
+ client.on("Page.javascriptDialogClosed", (_params, eventSession) => {
105477
+ if (eventSession === page.sessionId)
105478
+ openDialog = null;
105479
+ });
105480
+ const guarded = async (fn) => {
105481
+ if (sessionEnded)
105482
+ throw sessionEnded;
105483
+ try {
105484
+ return await fn();
105485
+ } catch (error40) {
105486
+ if (error40 instanceof CdpClosedError && !quitting)
105487
+ throw dead(error40.message);
105488
+ throw error40;
105489
+ }
105490
+ };
105491
+ const call = (method, params) => guarded(async () => {
105492
+ await switching;
105493
+ return client.send(method, params, page.sessionId);
105494
+ });
105495
+ const callBrowser = (method, params) => guarded(() => client.send(method, params));
105496
+ let chain = Promise.resolve();
105497
+ const withActionLock = (fn) => {
105498
+ const after = () => switching.then(fn);
105499
+ const run = chain.then(after, after);
105500
+ chain = run.catch(() => {
105501
+ return;
105502
+ });
105503
+ return run;
105504
+ };
105505
+ const getWindowSize = () => guarded(async () => {
105506
+ await switching;
105507
+ return { width: preset.width, height: preset.height };
105508
+ });
105509
+ const toPage = async (xNorm, yNorm) => {
105510
+ const viewport = await getWindowSize();
105511
+ return { x: xNorm / 1000 * viewport.width, y: yNorm / 1000 * viewport.height };
105512
+ };
105513
+ const mouse = (type, point, held = false) => {
105514
+ if (type === "mouseMoved") {
105515
+ return call("Input.dispatchMouseEvent", {
105516
+ type,
105517
+ ...point,
105518
+ button: held ? "left" : "none",
105519
+ buttons: held ? 1 : 0
105520
+ });
105521
+ }
105522
+ return call("Input.dispatchMouseEvent", {
105523
+ type,
105524
+ ...point,
105525
+ button: "left",
105526
+ buttons: type === "mousePressed" ? 1 : 0,
105527
+ clickCount: 1
105528
+ });
105529
+ };
105530
+ const touch = (type, point) => call("Input.dispatchTouchEvent", { type, touchPoints: type === "touchEnd" ? [] : [point] });
105531
+ const pointer = preset.mobile ? {
105532
+ press: (point) => touch("touchStart", point),
105533
+ move: (point) => touch("touchMove", point),
105534
+ release: (point) => touch("touchEnd", point)
105535
+ } : {
105536
+ press: async (point) => {
105537
+ await mouse("mouseMoved", point);
105538
+ await mouse("mousePressed", point);
105539
+ },
105540
+ move: (point) => mouse("mouseMoved", point, true),
105541
+ release: (point) => mouse("mouseReleased", point)
105542
+ };
105543
+ const click = async (xNorm, yNorm, holdMs = 0) => {
105544
+ const point = await toPage(xNorm, yNorm);
105545
+ await pointer.press(point);
105546
+ if (holdMs > 0)
105547
+ await Bun.sleep(holdMs);
105548
+ await pointer.release(point);
105549
+ };
105550
+ const stroke = async (from, to, durationMs) => {
105551
+ await pointer.press(from);
105552
+ for (let step = 1;step <= DRAG_STEPS; step++) {
105553
+ await Bun.sleep(durationMs / DRAG_STEPS);
105554
+ const t = step / DRAG_STEPS;
105555
+ await pointer.move({ x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t });
105556
+ }
105557
+ await pointer.release(to);
105558
+ };
105559
+ const scroll = async (x1, y1, x2, y2, durationMs = DEFAULT_SWIPE_MS) => {
105560
+ const from = await toPage(x1, y1);
105561
+ const to = await toPage(x2, y2);
105562
+ if (preset.mobile) {
105563
+ await stroke(from, to, durationMs);
105564
+ return;
105565
+ }
105566
+ await call("Input.dispatchMouseEvent", {
105567
+ type: "mouseWheel",
105568
+ ...from,
105569
+ deltaX: from.x - to.x,
105570
+ deltaY: from.y - to.y
105571
+ });
105572
+ };
105573
+ const drag = async (x1, y1, x2, y2, durationMs = DEFAULT_DRAG_MS) => {
105574
+ await stroke(await toPage(x1, y1), await toPage(x2, y2), durationMs);
105575
+ };
105576
+ const answerDialog = async (accept) => {
105577
+ const promptText = accept && openDialog?.type === "prompt" ? openDialog.defaultPrompt ?? "" : undefined;
105578
+ try {
105579
+ await call("Page.handleJavaScriptDialog", {
105580
+ accept,
105581
+ ...promptText !== undefined ? { promptText } : {}
105582
+ });
105583
+ } catch (error40) {
105584
+ if (error40 instanceof CdpCommandError && /no dialog is showing/i.test(error40.message)) {
105585
+ throw new Error("No JavaScript dialog is open on the page");
105586
+ }
105587
+ throw error40;
105588
+ }
105589
+ openDialog = null;
105590
+ };
105591
+ const navigate = async (url2) => {
105592
+ let loaded = () => {};
105593
+ const load = new Promise((resolve7) => {
105594
+ loaded = resolve7;
105595
+ });
105596
+ const stopListening = client.on("Page.loadEventFired", (_params, eventSession) => {
105597
+ if (eventSession === page.sessionId)
105598
+ loaded();
105599
+ });
105600
+ try {
105601
+ const { errorText, loaderId } = await call("Page.navigate", { url: url2 });
105602
+ if (errorText)
105603
+ throw new Error(`Could not open ${url2}: ${errorText}`);
105604
+ if (loaderId)
105605
+ await Promise.race([load, client.closed, Bun.sleep(NAVIGATE_LOAD_MS)]);
105606
+ } finally {
105607
+ stopListening();
105608
+ }
105609
+ };
105610
+ const closeAppPage = async () => {
105611
+ const appPage = page;
105612
+ const { targetId } = await callBrowser("Target.createTarget", { url: "about:blank" });
105613
+ page = await guarded(() => attachPage(client, targetId, preset));
105614
+ openDialog = null;
105615
+ await callBrowser("Target.closeTarget", { targetId: appPage.targetId });
105616
+ };
105617
+ const openApp = async (url2) => {
105618
+ const { targetInfo } = await callBrowser("Target.getTargetInfo", { targetId: page.targetId });
105619
+ if (originOf(targetInfo.url) === url2.origin)
105620
+ await call("Page.bringToFront", {});
105621
+ else
105622
+ await navigate(url2.href);
105623
+ };
105624
+ const pressKey = async (name50, modifiers = []) => {
105625
+ let held = 0;
105626
+ for (const modifier of modifiers) {
105627
+ const { bit, ...key2 } = CDP_MODIFIERS[modifier];
105628
+ held |= bit;
105629
+ await call("Input.dispatchKeyEvent", { type: "rawKeyDown", ...key2, modifiers: held });
105630
+ }
105631
+ const { text: text3, ...key } = CDP_KEYS[name50];
105632
+ const shortcut = modifiers.some((modifier) => modifier !== "Shift");
105633
+ await call("Input.dispatchKeyEvent", text3 && !shortcut ? { type: "keyDown", ...key, text: text3, modifiers: held } : { type: "rawKeyDown", ...key, modifiers: held });
105634
+ await call("Input.dispatchKeyEvent", { type: "keyUp", ...key, modifiers: held });
105635
+ for (const modifier of [...modifiers].reverse()) {
105636
+ const { bit, ...key2 } = CDP_MODIFIERS[modifier];
105637
+ held &= ~bit;
105638
+ await call("Input.dispatchKeyEvent", { type: "keyUp", ...key2, modifiers: held });
105639
+ }
105640
+ };
105641
+ const typeText = async (text3) => {
105642
+ for (const part2 of text3.split(NEWLINE)) {
105643
+ if (part2 === "")
105644
+ continue;
105645
+ if (part2 === `
105646
+ ` || part2 === `\r
105647
+ `)
105648
+ await pressKey("Enter");
105649
+ else
105650
+ await call("Input.insertText", { text: part2 });
105651
+ }
105652
+ };
105653
+ const captureFrame = async () => {
105654
+ const { data } = await call("Page.captureScreenshot", { format: "png" });
105655
+ if (!data)
105656
+ throw new Error("The browser returned an empty screenshot");
105657
+ return { base64: data, mime: "image/png" };
105658
+ };
105659
+ const screenshot = async () => {
105660
+ await mkdir6(SCREENSHOT_DIR, { recursive: true });
105661
+ const frame = await captureFrame();
105662
+ const path = join20(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
105663
+ await Bun.write(path, Uint8Array.from(Buffer.from(frame.base64, "base64")));
105664
+ return { path, base64: frame.base64 };
105665
+ };
105666
+ const pageSource = async () => {
105667
+ const [{ nodes }, snapshot] = await Promise.all([
105668
+ call("Accessibility.getFullAXTree", {}),
105669
+ call("DOMSnapshot.captureSnapshot", { computedStyles: [] })
105670
+ ]);
105671
+ const layouts = layoutByBackendId(snapshot.documents, snapshot.strings);
105672
+ const axTree = nodes.map((node) => {
105673
+ const layout = node.backendDOMNodeId === undefined ? undefined : layouts.get(node.backendDOMNodeId);
105674
+ return layout ? { ...node, ...layout } : node;
105675
+ });
105676
+ return JSON.stringify({ axTree });
105677
+ };
105678
+ let pointerDown = false;
105679
+ return {
105680
+ lane: "direct",
105681
+ stream: null,
105682
+ browser,
105683
+ viewport: preset.name,
105684
+ noRecordingReason: "Run recording is not available on web",
105685
+ quit: async () => {
105686
+ quitting = true;
105687
+ await client.close();
105688
+ await owned?.stop();
105689
+ },
105690
+ captureFrame,
105691
+ screenshot,
105692
+ pageSource,
105693
+ getWindowSize,
105694
+ tap: (x, y, tapOptions) => withActionLock(() => click(x, y, tapOptions?.durationMs)),
105695
+ swipe: (x1, y1, x2, y2, durationMs) => withActionLock(() => scroll(x1, y1, x2, y2, durationMs)),
105696
+ drag: (x1, y1, x2, y2, durationMs) => withActionLock(() => drag(x1, y1, x2, y2, durationMs)),
105697
+ type: (text3) => withActionLock(() => typeText(text3)),
105698
+ keySupport: WEB_KEY_SUPPORT,
105699
+ key: async (name50, modifiers = []) => {
105700
+ assertKeySupported(WEB_KEY_SUPPORT, "on the web", name50, modifiers);
105701
+ await withActionLock(() => pressKey(name50, modifiers));
105702
+ },
105703
+ activateApp: (appId) => withActionLock(() => openApp(webUrlOf(appId))),
105704
+ setTargetApp: () => {
105705
+ return;
105706
+ },
105707
+ terminateApp: (appId) => withActionLock(async () => {
105708
+ webUrlOf(appId);
105709
+ await closeAppPage();
105710
+ }),
105711
+ restartApp: (appId) => withActionLock(async () => {
105712
+ const url2 = webUrlOf(appId);
105713
+ await closeAppPage();
105714
+ await callBrowser("Storage.clearDataForOrigin", {
105715
+ origin: url2.origin,
105716
+ storageTypes: "all"
105717
+ });
105718
+ await navigate(url2.href);
105719
+ }),
105720
+ backgroundApp: () => Promise.reject(new ActionUnsupportedError("background-app is not supported on web: a browser page has no background to send it to")),
105721
+ openUrl: (url2) => withActionLock(() => navigate(url2)),
105722
+ acceptAlert: () => withActionLock(() => answerDialog(true)),
105723
+ dismissAlert: () => withActionLock(() => answerDialog(false)),
105724
+ withActionLock,
105725
+ pointerEvent: async (phase, xNorm, yNorm) => {
105726
+ const point = await toPage(xNorm, yNorm);
105727
+ if (phase === "begin") {
105728
+ pointerDown = true;
105729
+ await pointer.press(point);
105730
+ } else if (phase === "move") {
105731
+ await pointer.move(point);
105732
+ } else {
105733
+ pointerDown = false;
105734
+ await pointer.release(point);
105735
+ }
105736
+ },
105737
+ isPointerActive: () => pointerDown
105738
+ };
105739
+ }
105740
+
104504
105741
  // src/domains/devices/yoqa-ax.ts
104505
105742
  import { randomBytes } from "crypto";
104506
- import { existsSync as existsSync6, rmSync } from "fs";
105743
+ import { existsSync as existsSync6, rmSync as rmSync2 } from "fs";
104507
105744
  import { createServer } from "net";
104508
- import { join as join19 } from "path";
104509
- var CONNECT_TIMEOUT_MS = 1e4;
105745
+ import { join as join21 } from "path";
105746
+ var CONNECT_TIMEOUT_MS2 = 1e4;
104510
105747
  var REQUEST_TIMEOUT_MS2 = 5000;
104511
- var STOP_GRACE_MS2 = 2000;
105748
+ var STOP_GRACE_MS3 = 2000;
104512
105749
  function spawnProcess2(command) {
104513
105750
  return Bun.spawn(command, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
104514
105751
  }
@@ -104516,9 +105753,9 @@ function resolveYoqaAxBin() {
104516
105753
  const fromEnv = process.env.YOQA_AX_BIN?.trim();
104517
105754
  if (fromEnv)
104518
105755
  return fromEnv;
104519
- const products = join19(import.meta.dir, "../../../../../native/yoqa-ax/.build/out/Products");
105756
+ const products = join21(import.meta.dir, "../../../../../native/yoqa-ax/.build/out/Products");
104520
105757
  for (const config2 of ["Release-iphonesimulator", "Debug-iphonesimulator"]) {
104521
- const built = join19(products, config2, "yoqa-ax");
105758
+ const built = join21(products, config2, "yoqa-ax");
104522
105759
  if (existsSync6(built))
104523
105760
  return built;
104524
105761
  }
@@ -104635,15 +105872,15 @@ async function startYoqaAx(udid, options) {
104635
105872
  stopped = true;
104636
105873
  client?.close("is stopped");
104637
105874
  server.close();
104638
- rmSync(path, { force: true });
105875
+ rmSync2(path, { force: true });
104639
105876
  const exited = await Promise.race([
104640
105877
  proc.exited.then(() => true),
104641
- Bun.sleep(STOP_GRACE_MS2).then(() => false)
105878
+ Bun.sleep(STOP_GRACE_MS3).then(() => false)
104642
105879
  ]);
104643
105880
  if (!exited)
104644
105881
  proc.kill("SIGKILL");
104645
105882
  };
104646
- const timeoutMs = options.connectTimeoutMs ?? CONNECT_TIMEOUT_MS;
105883
+ const timeoutMs = options.connectTimeoutMs ?? CONNECT_TIMEOUT_MS2;
104647
105884
  let timer;
104648
105885
  const outcome = await Promise.race([
104649
105886
  connected,
@@ -104717,9 +105954,14 @@ function createDirectLane(implementations, optIn) {
104717
105954
  };
104718
105955
  }
104719
105956
  function deviceClassFor(platform, deviceId) {
104720
- if (platform === "android")
104721
- return "android";
104722
- return looksLikePhysicalIosUdid(deviceId) ? "ios-device" : "ios-simulator";
105957
+ switch (platform) {
105958
+ case "android":
105959
+ return "android";
105960
+ case "ios":
105961
+ return looksLikePhysicalIosUdid(deviceId) ? "ios-device" : "ios-simulator";
105962
+ case "web":
105963
+ return "web";
105964
+ }
104723
105965
  }
104724
105966
  var DIRECT_IMPLEMENTATIONS = {
104725
105967
  android: [
@@ -104758,6 +106000,13 @@ var DIRECT_IMPLEMENTATIONS = {
104758
106000
  promoted: true,
104759
106001
  open: (options) => createIosDeviceSession(options)
104760
106002
  }
106003
+ ],
106004
+ web: [
106005
+ {
106006
+ name: "cdp",
106007
+ promoted: true,
106008
+ open: (options) => createWebDirectSession(options)
106009
+ }
104761
106010
  ]
104762
106011
  };
104763
106012
  function defaultDirectLane(platform) {
@@ -104770,9 +106019,12 @@ function defaultDirectLane(platform) {
104770
106019
  // src/domains/devices/open-session.ts
104771
106020
  var openByDeviceId = new Map;
104772
106021
  function defaultLanesFor(platform) {
104773
- return {
104774
- ...platform === "android" || platform === "ios" ? { direct: defaultDirectLane(platform) } : {}
104775
- };
106022
+ switch (platform) {
106023
+ case "android":
106024
+ case "ios":
106025
+ case "web":
106026
+ return { direct: defaultDirectLane(platform) };
106027
+ }
104776
106028
  }
104777
106029
  function mergeLanes(platform, lanes) {
104778
106030
  const defaults = defaultLanesFor(platform);
@@ -104868,7 +106120,8 @@ function toInfo(current) {
104868
106120
  heldByRun: current.heldByRunId != null,
104869
106121
  ...current.heldByRunId != null ? { heldByRunId: current.heldByRunId } : {},
104870
106122
  lane: current.session.lane,
104871
- ...current.session.laneWarning ? { laneWarning: current.session.laneWarning } : {}
106123
+ ...current.session.laneWarning ? { laneWarning: current.session.laneWarning } : {},
106124
+ ...current.session.viewport ? { viewport: current.session.viewport } : {}
104872
106125
  };
104873
106126
  }
104874
106127
  function getActiveSession() {
@@ -104904,6 +106157,7 @@ async function createAndRegister(options) {
104904
106157
  bundleId: options.bundleId,
104905
106158
  appPackage: options.appPackage,
104906
106159
  requestedLane: options.requestedLane,
106160
+ viewport: options.viewport,
104907
106161
  onSessionDead: () => {
104908
106162
  abandonActiveSession();
104909
106163
  }
@@ -105001,7 +106255,14 @@ async function acquireSessionForRun(options) {
105001
106255
  return { session, shared: true };
105002
106256
  }
105003
106257
  function targetAppFor(options) {
105004
- return options.platform === "ios" ? options.bundleId : options.appPackage;
106258
+ switch (options.platform) {
106259
+ case "ios":
106260
+ return options.bundleId;
106261
+ case "android":
106262
+ return options.appPackage;
106263
+ case "web":
106264
+ return;
106265
+ }
105005
106266
  }
105006
106267
  async function releaseSessionFromRun(runId, session, shared) {
105007
106268
  if (!shared) {
@@ -105021,11 +106282,11 @@ import {
105021
106282
  createDecipheriv,
105022
106283
  randomBytes as randomBytes2
105023
106284
  } from "crypto";
105024
- import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
105025
- import { homedir as homedir10 } from "os";
105026
- import { dirname as dirname6, join as join20 } from "path";
105027
- var YOQA_ROOT2 = join20(homedir10(), ".yoqa");
105028
- var KEY_PATH = join20(YOQA_ROOT2, ".provider-key");
106285
+ import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
106286
+ import { homedir as homedir11 } from "os";
106287
+ import { dirname as dirname6, join as join22 } from "path";
106288
+ var YOQA_ROOT2 = join22(homedir11(), ".yoqa");
106289
+ var KEY_PATH = join22(YOQA_ROOT2, ".provider-key");
105029
106290
  var ALGORITHM = "aes-256-gcm";
105030
106291
  var IV_LENGTH = 12;
105031
106292
  var KEY_LENGTH = 32;
@@ -105058,9 +106319,9 @@ function base64ToBytes(value) {
105058
106319
  return bytes;
105059
106320
  }
105060
106321
  function ensureProviderKey() {
105061
- mkdirSync2(YOQA_ROOT2, { recursive: true });
106322
+ mkdirSync3(YOQA_ROOT2, { recursive: true });
105062
106323
  if (existsSync7(KEY_PATH)) {
105063
- const existing = toBytes(readFileSync(KEY_PATH));
106324
+ const existing = toBytes(readFileSync2(KEY_PATH));
105064
106325
  if (existing.length === KEY_LENGTH) {
105065
106326
  return existing;
105066
106327
  }
@@ -105619,8 +106880,12 @@ async function performAction(session, body, options) {
105619
106880
  case "restart-app": {
105620
106881
  if (!body.appId)
105621
106882
  throw new ActionValidationError("restart-app requires appId");
105622
- await session.terminateApp(body.appId);
105623
- await session.activateApp(body.appId);
106883
+ if (session.restartApp) {
106884
+ await session.restartApp(body.appId);
106885
+ } else {
106886
+ await session.terminateApp(body.appId);
106887
+ await session.activateApp(body.appId);
106888
+ }
105624
106889
  break;
105625
106890
  }
105626
106891
  case "background-app": {
@@ -105633,6 +106898,18 @@ async function performAction(session, body, options) {
105633
106898
  await session.openUrl(body.url);
105634
106899
  break;
105635
106900
  }
106901
+ case "key": {
106902
+ if (!body.key)
106903
+ throw new ActionValidationError("key requires a key");
106904
+ try {
106905
+ await session.key(body.key, body.modifiers);
106906
+ } catch (error40) {
106907
+ if (error40 instanceof UnsupportedKeyError)
106908
+ throw new ActionValidationError(error40.message);
106909
+ throw error40;
106910
+ }
106911
+ break;
106912
+ }
105636
106913
  case "alert": {
105637
106914
  if (body.alertAction === "dismiss") {
105638
106915
  await session.dismissAlert();
@@ -105686,37 +106963,18 @@ async function readCleanedTree(readScreen2, session) {
105686
106963
  }
105687
106964
  }
105688
106965
  async function runTextAssert(input) {
105689
- const deadline = input.clock.now() + input.timeoutMs;
105690
- const seconds = Math.round(input.timeoutMs / 1000);
105691
- for (;; ) {
105692
- if (input.isAborted()) {
105693
- throw new Error("Aborted");
105694
- }
105695
- let readError = null;
105696
- let elements = null;
105697
- try {
105698
- elements = (await input.readScreen(input.session)).elements ?? [];
105699
- } catch (error40) {
105700
- if (isDeadSessionError(error40))
105701
- throw error40;
105702
- readError = error40;
105703
- }
105704
- if (elements) {
105705
- const found = screenHasText(elements, input.text);
105706
- if (input.assertion === "visible" && found)
105707
- return;
105708
- if (input.assertion === "not-visible" && !found)
105709
- return;
105710
- }
105711
- if (input.clock.now() >= deadline) {
105712
- if (readError) {
105713
- const message = readError instanceof Error ? readError.message : String(readError);
105714
- throw new Error(`Could not read the screen within ${seconds}s: ${message}`);
105715
- }
105716
- throw new Error(input.assertion === "visible" ? `Expected visible text not found within ${seconds}s: ${input.text}` : `Unexpected text still visible after ${seconds}s: ${input.text}`);
105717
- }
105718
- await input.clock.sleep(400);
105719
- }
106966
+ const { met } = await checkScreen({
106967
+ readScreen: () => input.readScreen(input.session),
106968
+ text: input.text,
106969
+ assertion: input.assertion,
106970
+ timeoutMs: input.timeoutMs,
106971
+ sleep: (ms) => input.clock.sleep(ms),
106972
+ now: () => input.clock.now(),
106973
+ isAborted: input.isAborted,
106974
+ isDeadSession: isDeadSessionError
106975
+ });
106976
+ if (!met)
106977
+ throw new Error(screenCheckMissMessage(input));
105720
106978
  }
105721
106979
  function isPointTap(decision) {
105722
106980
  return decision.type === "tap" && decision.x != null && decision.y != null && !isSystemPermissionLabel(decision.label);
@@ -105755,11 +107013,61 @@ async function executeScriptCase(deps) {
105755
107013
  const setCurrentCommand = deps.setCurrentCommand ?? noopSetCurrentCommand;
105756
107014
  let stepIdx = 0;
105757
107015
  let lastScreenshotUri = null;
105758
- try {
105759
- for (const action of deps.script.actions) {
107016
+ const replay = async (actions) => {
107017
+ for (const action of actions) {
105760
107018
  if (deps.isAborted()) {
105761
107019
  return "cancelled";
105762
107020
  }
107021
+ if (action.type === "branch") {
107022
+ const timeoutMs = action.timeoutMs ?? BRANCH_DEFAULT_TIMEOUT_MS;
107023
+ const { assertion } = action;
107024
+ const target = action.id != null ? { id: action.id } : { text: action.text };
107025
+ const command = formatAssertShellLine({
107026
+ assertion,
107027
+ ...target,
107028
+ timeoutSeconds: timeoutMs / 1000
107029
+ });
107030
+ const check2 = describeScreenCheck({ assertion, ...target });
107031
+ const checkStarted = clock.now();
107032
+ const { met } = await withCurrentCommand(setCurrentCommand, command, () => checkScreen({
107033
+ readScreen: () => readScreen2(deps.session),
107034
+ ...target,
107035
+ assertion,
107036
+ timeoutMs,
107037
+ sleep: (ms) => clock.sleep(ms),
107038
+ now: () => clock.now(),
107039
+ isAborted: deps.isAborted,
107040
+ isDeadSession: isDeadSessionError
107041
+ }));
107042
+ const shot2 = await deps.session.screenshot();
107043
+ lastScreenshotUri = shot2.path;
107044
+ if (deps.isAborted()) {
107045
+ return "cancelled";
107046
+ }
107047
+ const outcome = met ? "then" : "else";
107048
+ await deps.appendStep({
107049
+ idx: stepIdx,
107050
+ action: {
107051
+ type: "branch",
107052
+ ...target,
107053
+ assertion,
107054
+ timeoutMs,
107055
+ outcome,
107056
+ reason: action.reason ?? `If ${check2}`,
107057
+ thoughts: "Replaying the saved script without calling the AI agent."
107058
+ },
107059
+ screenshotUri: shot2.path,
107060
+ ok: true,
107061
+ latencyMs: clock.now() - checkStarted,
107062
+ detail: `${check2} \u2192 ${outcome}`,
107063
+ command
107064
+ });
107065
+ stepIdx += 1;
107066
+ const stopped = await replay((met ? action.then : action.else) ?? []);
107067
+ if (stopped)
107068
+ return stopped;
107069
+ continue;
107070
+ }
105763
107071
  const shotStarted = clock.now();
105764
107072
  const shot = await deps.session.screenshot();
105765
107073
  lastScreenshotUri = shot.path;
@@ -105933,6 +107241,32 @@ async function executeScriptCase(deps) {
105933
107241
  command
105934
107242
  });
105935
107243
  });
107244
+ } else if (action.type === "key") {
107245
+ const keyBody = {
107246
+ kind: "key",
107247
+ key: action.key,
107248
+ ...action.modifiers?.length ? { modifiers: action.modifiers } : {}
107249
+ };
107250
+ const command = formatActionShellLine(keyBody);
107251
+ await withCurrentCommand(setCurrentCommand, command, async () => {
107252
+ await perform(deps.session, keyBody);
107253
+ await clock.sleep(settleMs);
107254
+ await deps.appendStep({
107255
+ idx: stepIdx,
107256
+ action: {
107257
+ type: "key",
107258
+ key: action.key,
107259
+ modifiers: action.modifiers,
107260
+ reason: action.reason ?? "Replayed saved script key",
107261
+ thoughts: "Replaying the saved script without calling the AI agent."
107262
+ },
107263
+ screenshotUri: shot.path,
107264
+ ok: true,
107265
+ latencyMs,
107266
+ detail: action.reason ?? null,
107267
+ command
107268
+ });
107269
+ });
105936
107270
  } else if (action.type === "type") {
105937
107271
  const typeBody = { kind: "input", text: action.text };
105938
107272
  const command = formatActionShellLine(typeBody);
@@ -106048,6 +107382,13 @@ async function executeScriptCase(deps) {
106048
107382
  }
106049
107383
  stepIdx += 1;
106050
107384
  }
107385
+ return null;
107386
+ };
107387
+ try {
107388
+ const stopped = await replay(deps.script.actions);
107389
+ if (stopped) {
107390
+ return stopped;
107391
+ }
106051
107392
  await deps.appendStep({
106052
107393
  idx: stepIdx,
106053
107394
  action: {
@@ -106635,12 +107976,12 @@ function reportedLaneWarning(warning) {
106635
107976
 
106636
107977
  // src/domains/runs/run-recording.ts
106637
107978
  import { existsSync as existsSync8 } from "fs";
106638
- import { rm as rm7 } from "fs/promises";
106639
- import { homedir as homedir11 } from "os";
106640
- import { join as join21 } from "path";
106641
- var RUN_VIDEO_DIR = join21(homedir11(), ".yoqa", "runs", "videos");
107979
+ import { rm as rm8 } from "fs/promises";
107980
+ import { homedir as homedir12 } from "os";
107981
+ import { join as join23 } from "path";
107982
+ var RUN_VIDEO_DIR = join23(homedir12(), ".yoqa", "runs", "videos");
106642
107983
  function runVideoPath(runTestId, dir = RUN_VIDEO_DIR) {
106643
- return join21(dir, `${runTestId}.mp4`);
107984
+ return join23(dir, `${runTestId}.mp4`);
106644
107985
  }
106645
107986
  var STOP_TIMEOUT_MS = 20000;
106646
107987
  function errorMessage7(error40) {
@@ -106648,7 +107989,9 @@ function errorMessage7(error40) {
106648
107989
  }
106649
107990
  async function startRunRecording(session, path) {
106650
107991
  if (!session.startRecording) {
106651
- return { unavailable: `The ${session.lane} lane cannot record video` };
107992
+ return {
107993
+ unavailable: session.noRecordingReason ?? `The ${session.lane} lane cannot record video`
107994
+ };
106652
107995
  }
106653
107996
  let recording;
106654
107997
  try {
@@ -106682,7 +108025,7 @@ async function startRunRecording(session, path) {
106682
108025
  };
106683
108026
  }
106684
108027
  async function deleteRunVideo(runTestId, dir = RUN_VIDEO_DIR) {
106685
- await rm7(runVideoPath(runTestId, dir), { force: true }).catch(() => {
108028
+ await rm8(runVideoPath(runTestId, dir), { force: true }).catch(() => {
106686
108029
  return;
106687
108030
  });
106688
108031
  }
@@ -106780,6 +108123,10 @@ function parseRunExecutionMode(value) {
106780
108123
  function parseRunScreenMode(value) {
106781
108124
  return value === "vision" ? "vision" : "tree";
106782
108125
  }
108126
+ function viewportOf(value) {
108127
+ const parsed = viewportPresetSchema.safeParse(value);
108128
+ return parsed.success ? { viewport: parsed.data } : {};
108129
+ }
106783
108130
  function resolveCaseExecutionMode(runMode, hasScript) {
106784
108131
  if (runMode === "agent")
106785
108132
  return "agent";
@@ -106838,6 +108185,8 @@ async function loadRun(runId) {
106838
108185
  screenMode: parseRunScreenMode(runRow.screenMode),
106839
108186
  lane: reportedLane(runRow.lane),
106840
108187
  laneWarning: reportedLaneWarning(runRow.laneWarning),
108188
+ ...runRow.browser ? { browser: runRow.browser } : {},
108189
+ ...viewportOf(runRow.viewport),
106841
108190
  error: runRow.error,
106842
108191
  createdAt: runRow.createdAt,
106843
108192
  startedAt: runRow.startedAt,
@@ -107010,6 +108359,17 @@ async function executeCase(input) {
107010
108359
  async function saveCaseRecording(runTestId, state) {
107011
108360
  await getCatalogDb().update(runTests).set({ recordingStatus: state.status, recordingNote: state.note ?? null }).where(eq(runTests.id, runTestId));
107012
108361
  }
108362
+ async function openStartTarget(session, platform, startTarget) {
108363
+ switch (platform) {
108364
+ case "ios":
108365
+ case "android":
108366
+ return;
108367
+ case "web":
108368
+ if (startTarget)
108369
+ await session.openUrl(startTarget);
108370
+ return;
108371
+ }
108372
+ }
107013
108373
  async function executeRun(runId) {
107014
108374
  const db = getCatalogDb();
107015
108375
  const run = await loadRun(runId);
@@ -107034,6 +108394,10 @@ async function executeRun(runId) {
107034
108394
  if (!app) {
107035
108395
  throw new RunValidationError("App not found");
107036
108396
  }
108397
+ const startTarget = appStartTarget(app, run.platform);
108398
+ if (run.platform === "web" && !startTarget) {
108399
+ throw new RunValidationError(`${app.name} has no Web URL, so a Run on web has nothing to open. Set one with: yoqa apps update ${app.prefix} --web-url <url>`);
108400
+ }
107037
108401
  const caseModes = [];
107038
108402
  for (const test of run.tests) {
107039
108403
  const catalogCase = await getCase(test.caseId);
@@ -107064,8 +108428,7 @@ async function executeRun(runId) {
107064
108428
  runId,
107065
108429
  deviceId: run.deviceId,
107066
108430
  platform: run.platform,
107067
- bundleId: app.iosBundleId || undefined,
107068
- appPackage: app.androidApplicationId || undefined,
108431
+ ...startTargetRequest(run.platform, startTarget),
107069
108432
  requestedLane: requestedLane === "direct" || requestedLane === "auto" ? requestedLane : undefined
107070
108433
  });
107071
108434
  const activeSession = acquired.session;
@@ -107073,8 +108436,11 @@ async function executeRun(runId) {
107073
108436
  sharedSession = acquired.shared;
107074
108437
  await db.update(runs).set({
107075
108438
  lane: session.lane,
107076
- laneWarning: session.laneWarning ?? null
108439
+ laneWarning: session.laneWarning ?? null,
108440
+ browser: session.browser ?? null,
108441
+ viewport: session.viewport ?? null
107077
108442
  }).where(eq(runs.id, runId));
108443
+ await openStartTarget(session, run.platform, startTarget);
107078
108444
  if (isAborted2(runId)) {
107079
108445
  await persistCancelled(runId);
107080
108446
  return;
@@ -107109,7 +108475,7 @@ async function executeRun(runId) {
107109
108475
  auth,
107110
108476
  caseMode,
107111
108477
  screenMode: run.screenMode ?? "tree",
107112
- defaultAppId: run.platform === "ios" ? app.iosBundleId || undefined : app.androidApplicationId || undefined
108478
+ defaultAppId: startTarget
107113
108479
  }));
107114
108480
  if (!await loadRun(runId))
107115
108481
  await deleteRunVideo(test.id);
@@ -107296,6 +108662,8 @@ async function listRuns(appId) {
107296
108662
  screenMode: parseRunScreenMode(runRow.screenMode),
107297
108663
  lane: reportedLane(runRow.lane),
107298
108664
  laneWarning: reportedLaneWarning(runRow.laneWarning),
108665
+ ...runRow.browser ? { browser: runRow.browser } : {},
108666
+ ...viewportOf(runRow.viewport),
107299
108667
  error: runRow.error,
107300
108668
  createdAt: runRow.createdAt,
107301
108669
  startedAt: runRow.startedAt,
@@ -109347,7 +110715,7 @@ function createDevicesRoutes() {
109347
110715
  const platformParam = c.req.query("platform");
109348
110716
  const parsedPlatform = devicePlatformSchema.safeParse(platformParam);
109349
110717
  if (!parsedPlatform.success) {
109350
- return c.json({ error: "Query param platform is required and must be 'ios' or 'android'" }, 400);
110718
+ return c.json({ error: "Query param platform is required and must be 'ios', 'android' or 'web'" }, 400);
109351
110719
  }
109352
110720
  const includeUnavailable = c.req.query("all") !== "0";
109353
110721
  const platform = parsedPlatform.data;
@@ -109362,12 +110730,12 @@ function createDevicesRoutes() {
109362
110730
  }
109363
110731
 
109364
110732
  // src/domains/doctor/application.ts
109365
- import { homedir as homedir12 } from "os";
109366
- import { join as join22 } from "path";
110733
+ import { homedir as homedir13 } from "os";
110734
+ import { join as join24 } from "path";
109367
110735
  // package.json
109368
110736
  var package_default = {
109369
110737
  name: "@yoqa/runner",
109370
- version: "1.0.0",
110738
+ version: "1.0.3",
109371
110739
  description: "Yoqa local runner \u2014 device control + catalog HTTP API (Bun). Start with yoqa serve or the yoqa-runner bin.",
109372
110740
  license: "UNLICENSED",
109373
110741
  type: "module",
@@ -109566,7 +110934,7 @@ async function probeJava() {
109566
110934
  let detail = javaBin ?? "java not on PATH";
109567
110935
  if (javaHome)
109568
110936
  detail = `JAVA_HOME=${javaHome}`;
109569
- else if (await Bun.file(join22(studioJbr, "bin/java")).exists()) {
110937
+ else if (await Bun.file(join24(studioJbr, "bin/java")).exists()) {
109570
110938
  detail = `Android Studio JBR available at ${studioJbr} (JAVA_HOME unset)`;
109571
110939
  }
109572
110940
  const ok = Boolean(javaBin || javaHome);
@@ -109597,7 +110965,7 @@ async function probeXcodeSelect() {
109597
110965
  };
109598
110966
  }
109599
110967
  async function probeAdbDevices() {
109600
- const adb = await which2("adb") ?? join22(homedir12(), "Library/Android/sdk/platform-tools/adb");
110968
+ const adb = await which2("adb") ?? join24(homedir13(), "Library/Android/sdk/platform-tools/adb");
109601
110969
  if (!await Bun.file(adb).exists() && !await which2("adb")) {
109602
110970
  return {
109603
110971
  id: "adb-devices",
@@ -110014,7 +111382,7 @@ function createProviderRoutes() {
110014
111382
  }
110015
111383
 
110016
111384
  // src/interfaces/http/runs.ts
110017
- import { readFile as readFile5 } from "fs/promises";
111385
+ import { readFile as readFile6 } from "fs/promises";
110018
111386
  function parseByteRange(header, size) {
110019
111387
  const match2 = header?.match(/^bytes=(\d*)-(\d*)$/);
110020
111388
  if (!match2)
@@ -110168,7 +111536,7 @@ function createRunsRoutes() {
110168
111536
  const stepId = c.req.param("stepId");
110169
111537
  try {
110170
111538
  const path = await getRunStepScreenshotPath(runId, stepId);
110171
- const bytes = await readFile5(path);
111539
+ const bytes = await readFile6(path);
110172
111540
  return new Response(bytes, {
110173
111541
  status: 200,
110174
111542
  headers: {
@@ -110231,17 +111599,17 @@ function createServersRoutes() {
110231
111599
  }
110232
111600
 
110233
111601
  // src/interfaces/http/session.ts
110234
- import { mkdir as mkdir7, rename as rename2 } from "fs/promises";
111602
+ import { mkdir as mkdir8, rename as rename2 } from "fs/promises";
110235
111603
  import { dirname as dirname7 } from "path";
110236
111604
 
110237
111605
  // src/domains/devices/action-result.ts
110238
- import { mkdir as mkdir6 } from "fs/promises";
110239
- import { join as join24 } from "path";
111606
+ import { mkdir as mkdir7 } from "fs/promises";
111607
+ import { join as join26 } from "path";
110240
111608
 
110241
111609
  // src/domains/devices/agent-image.ts
110242
- import { mkdtemp as mkdtemp6, readFile as readFile6, rm as rm8, writeFile as writeFile5 } from "fs/promises";
110243
- import { tmpdir as tmpdir7 } from "os";
110244
- import { join as join23 } from "path";
111610
+ import { mkdtemp as mkdtemp7, readFile as readFile7, rm as rm9, writeFile as writeFile5 } from "fs/promises";
111611
+ import { tmpdir as tmpdir8 } from "os";
111612
+ import { join as join25 } from "path";
110245
111613
  var DEFAULT_AGENT_IMAGE_EDGE = 1000;
110246
111614
  function targetEdge(pngBase64, options) {
110247
111615
  if (options.full || options.scale === 1)
@@ -110260,9 +111628,9 @@ async function prepareAgentImage(pngBase64, options = {}) {
110260
111628
  if (bytes.byteLength === 0 || edge == null || process.platform !== "darwin") {
110261
111629
  return { base64: pngBase64, mediaType: "image/png", downscaled: false };
110262
111630
  }
110263
- const dir = await mkdtemp6(join23(tmpdir7(), "yoqa-agent-image-"));
110264
- const inPath = join23(dir, "shot.png");
110265
- const outPath = join23(dir, "shot.jpg");
111631
+ const dir = await mkdtemp7(join25(tmpdir8(), "yoqa-agent-image-"));
111632
+ const inPath = join25(dir, "shot.png");
111633
+ const outPath = join25(dir, "shot.jpg");
110266
111634
  try {
110267
111635
  await writeFile5(inPath, new Uint8Array(bytes));
110268
111636
  const proc = Bun.spawn([
@@ -110282,7 +111650,7 @@ async function prepareAgentImage(pngBase64, options = {}) {
110282
111650
  if (await proc.exited !== 0) {
110283
111651
  return { base64: pngBase64, mediaType: "image/png", downscaled: false };
110284
111652
  }
110285
- const jpeg = await readFile6(outPath);
111653
+ const jpeg = await readFile7(outPath);
110286
111654
  if (jpeg.byteLength === 0 || jpeg.byteLength >= bytes.byteLength) {
110287
111655
  return { base64: pngBase64, mediaType: "image/png", downscaled: false };
110288
111656
  }
@@ -110290,7 +111658,7 @@ async function prepareAgentImage(pngBase64, options = {}) {
110290
111658
  } catch {
110291
111659
  return { base64: pngBase64, mediaType: "image/png", downscaled: false };
110292
111660
  } finally {
110293
- await rm8(dir, { recursive: true, force: true }).catch(() => {
111661
+ await rm9(dir, { recursive: true, force: true }).catch(() => {
110294
111662
  return;
110295
111663
  });
110296
111664
  }
@@ -110351,8 +111719,8 @@ function actionMarks(body, resolved) {
110351
111719
  return [];
110352
111720
  }
110353
111721
  async function writeImage(dir, base643, label, ext) {
110354
- await mkdir6(dir, { recursive: true });
110355
- const path = join24(dir, `${label}_${Date.now()}_${crypto.randomUUID()}.${ext}`);
111722
+ await mkdir7(dir, { recursive: true });
111723
+ const path = join26(dir, `${label}_${Date.now()}_${crypto.randomUUID()}.${ext}`);
110356
111724
  await Bun.write(path, Uint8Array.from(Buffer.from(base643, "base64")));
110357
111725
  return path;
110358
111726
  }
@@ -110523,6 +111891,9 @@ function createSessionRoutes() {
110523
111891
  }
110524
111892
  const parsed = connectDeviceRequestSchema.safeParse(json3);
110525
111893
  if (!parsed.success) {
111894
+ if (parsed.error.issues.some((issue2) => issue2.path[0] === "viewport")) {
111895
+ return c.json({ error: `viewport must be ${VIEWPORT_PRESET_NAMES.join(" or ")}` }, 400);
111896
+ }
110526
111897
  return c.json({ error: "Body must include deviceId and platform" }, 400);
110527
111898
  }
110528
111899
  try {
@@ -110617,7 +111988,7 @@ function createSessionRoutes() {
110617
111988
  const shot = await session.screenshot();
110618
111989
  let path = shot.path;
110619
111990
  if (parsed.data.path) {
110620
- await mkdir7(dirname7(parsed.data.path), { recursive: true });
111991
+ await mkdir8(dirname7(parsed.data.path), { recursive: true });
110621
111992
  await rename2(shot.path, parsed.data.path);
110622
111993
  path = parsed.data.path;
110623
111994
  }
@@ -110762,6 +112133,9 @@ function createSessionRoutes() {
110762
112133
  if (error40 instanceof ActionNotFoundError) {
110763
112134
  return c.json({ error: error40.message }, 404);
110764
112135
  }
112136
+ if (error40 instanceof ActionUnsupportedError) {
112137
+ return c.json({ error: "Action not supported", detail: error40.message }, 422);
112138
+ }
110765
112139
  const message = error40 instanceof Error ? error40.message : String(error40);
110766
112140
  return c.json({ error: "Action failed", detail: message }, 500);
110767
112141
  }