@validate.qa/runner 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +450 -334
  2. package/dist/cli.mjs +455 -339
  3. package/package.json +2 -2
package/dist/cli.mjs CHANGED
@@ -1410,7 +1410,7 @@ function selectorForValue(value) {
1410
1410
  if (value.startsWith("~") || value.startsWith("id="))
1411
1411
  return value;
1412
1412
  if (value.includes(":id/"))
1413
- return `id=${value.split(":id/")[1]}`;
1413
+ return `id=${value}`;
1414
1414
  return `~${value}`;
1415
1415
  }
1416
1416
  function selectorCandidatesForStep(step) {
@@ -7557,7 +7557,7 @@ Each entry is exactly one of:
7557
7557
  const systemPrompt = context.mode === "survey" ? SURVEY_SYSTEM_PROMPT : exports.SCENARIO_SYSTEM_PROMPT;
7558
7558
  const userPrompt = formatEvidenceForPlanner(compressed, evidence, context);
7559
7559
  logs2.push(`AI planner: ~${Math.ceil(userPrompt.length / 3.5)} input tokens`);
7560
- const aiClient = options.modelOverride ? (0, ai_provider_js_1.getClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getAIClient)();
7560
+ const aiClient = options.aiClient ?? (options.modelOverride ? (0, ai_provider_js_1.getClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getAIClient)());
7561
7561
  const messages = [
7562
7562
  { role: "system", content: systemPrompt },
7563
7563
  { role: "user", content: userPrompt }
@@ -7769,6 +7769,7 @@ var require_mobile_discovery_prompts = __commonJS({
7769
7769
  exports.formatMobileEvidenceForPlanner = formatMobileEvidenceForPlanner;
7770
7770
  exports.buildMobileSiteMap = buildMobileSiteMap;
7771
7771
  exports.runMobileAIPlanner = runMobileAIPlanner;
7772
+ var ai_provider_js_1 = require_ai_provider();
7772
7773
  var discover_ai_planner_js_1 = require_discover_ai_planner();
7773
7774
  exports.MOBILE_SURVEY_SYSTEM_PROMPT = `You are a feature analyst for a native mobile application (iOS or Android). You receive evidence from an AI explorer that mapped the app's screens by tapping, typing, and swiping through the live UI. Your job is to organize screens into meaningful feature groups.
7774
7775
 
@@ -8168,6 +8169,8 @@ ${constraints.join("\n")}`);
8168
8169
  }
8169
8170
  async function runMobileAIPlanner(input, options = {}) {
8170
8171
  const planner = options.planner ?? discover_ai_planner_js_1.runAIPlanner;
8172
+ const shouldBuildMobileAIClient = (process.env.SERVER_URL ?? "").trim().length > 0 || !options.planner;
8173
+ const aiClient = options.aiClient ?? (shouldBuildMobileAIClient ? options.modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getMobileAIClient)() : void 0);
8171
8174
  const siteMap = buildMobileSiteMap(input.graph, {
8172
8175
  projectId: input.projectId,
8173
8176
  appId: input.appId
@@ -8193,7 +8196,8 @@ ${constraints.join("\n")}`);
8193
8196
  testBudget: input.testBudget
8194
8197
  }, {
8195
8198
  modelOverride: options.modelOverride,
8196
- onAICall: options.onAICall
8199
+ onAICall: options.onAICall,
8200
+ ...aiClient ? { aiClient } : {}
8197
8201
  });
8198
8202
  }
8199
8203
  }
@@ -21743,7 +21747,7 @@ var require_util = __commonJS({
21743
21747
  return path;
21744
21748
  }
21745
21749
  exports.normalize = normalize;
21746
- function join4(aRoot, aPath) {
21750
+ function join5(aRoot, aPath) {
21747
21751
  if (aRoot === "") {
21748
21752
  aRoot = ".";
21749
21753
  }
@@ -21775,7 +21779,7 @@ var require_util = __commonJS({
21775
21779
  }
21776
21780
  return joined;
21777
21781
  }
21778
- exports.join = join4;
21782
+ exports.join = join5;
21779
21783
  exports.isAbsolute = function(aPath) {
21780
21784
  return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
21781
21785
  };
@@ -21948,7 +21952,7 @@ var require_util = __commonJS({
21948
21952
  parsed.path = parsed.path.substring(0, index + 1);
21949
21953
  }
21950
21954
  }
21951
- sourceURL = join4(urlGenerate(parsed), sourceURL);
21955
+ sourceURL = join5(urlGenerate(parsed), sourceURL);
21952
21956
  }
21953
21957
  return normalize(sourceURL);
21954
21958
  }
@@ -243103,7 +243107,7 @@ var require_thread_stream = __commonJS({
243103
243107
  var { version } = require_package();
243104
243108
  var { EventEmitter: EventEmitter2 } = __require("events");
243105
243109
  var { Worker } = __require("worker_threads");
243106
- var { join: join4 } = __require("path");
243110
+ var { join: join5 } = __require("path");
243107
243111
  var { pathToFileURL } = __require("url");
243108
243112
  var { wait } = require_wait();
243109
243113
  var {
@@ -243154,7 +243158,7 @@ var require_thread_stream = __commonJS({
243154
243158
  function createWorker(stream, opts) {
243155
243159
  const { filename, workerData } = opts;
243156
243160
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
243157
- const toExecute = bundlerOverrides["thread-stream-worker"] || join4(__dirname, "lib", "worker.js");
243161
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join5(__dirname, "lib", "worker.js");
243158
243162
  const worker = new Worker(toExecute, {
243159
243163
  ...opts.workerOpts,
243160
243164
  name: opts.workerOpts?.name || "thread-stream",
@@ -243622,7 +243626,7 @@ var require_transport = __commonJS({
243622
243626
  var { createRequire } = __require("module");
243623
243627
  var { existsSync: existsSync4 } = __require("fs");
243624
243628
  var getCallers = require_caller();
243625
- var { join: join4, isAbsolute, sep } = __require("path");
243629
+ var { join: join5, isAbsolute, sep } = __require("path");
243626
243630
  var { fileURLToPath } = __require("url");
243627
243631
  var sleep = require_atomic_sleep();
243628
243632
  var onExit = require_on_exit_leak_free();
@@ -243775,7 +243779,7 @@ var require_transport = __commonJS({
243775
243779
  throw new Error("only one of target or targets can be specified");
243776
243780
  }
243777
243781
  if (targets) {
243778
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
243782
+ target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
243779
243783
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
243780
243784
  return {
243781
243785
  ...dest,
@@ -243793,7 +243797,7 @@ var require_transport = __commonJS({
243793
243797
  });
243794
243798
  });
243795
243799
  } else if (pipeline) {
243796
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
243800
+ target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
243797
243801
  options.pipelines = [pipeline.map((dest) => {
243798
243802
  return {
243799
243803
  ...dest,
@@ -243816,7 +243820,7 @@ var require_transport = __commonJS({
243816
243820
  return origin;
243817
243821
  }
243818
243822
  if (origin === "pino/file") {
243819
- return join4(__dirname, "..", "file.js");
243823
+ return join5(__dirname, "..", "file.js");
243820
243824
  }
243821
243825
  let fixTarget2;
243822
243826
  for (const filePath of callers) {
@@ -244797,7 +244801,7 @@ var require_safe_stable_stringify = __commonJS({
244797
244801
  return circularValue;
244798
244802
  }
244799
244803
  let res = "";
244800
- let join4 = ",";
244804
+ let join5 = ",";
244801
244805
  const originalIndentation = indentation;
244802
244806
  if (Array.isArray(value)) {
244803
244807
  if (value.length === 0) {
@@ -244811,7 +244815,7 @@ var require_safe_stable_stringify = __commonJS({
244811
244815
  indentation += spacer;
244812
244816
  res += `
244813
244817
  ${indentation}`;
244814
- join4 = `,
244818
+ join5 = `,
244815
244819
  ${indentation}`;
244816
244820
  }
244817
244821
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -244819,13 +244823,13 @@ ${indentation}`;
244819
244823
  for (; i < maximumValuesToStringify - 1; i++) {
244820
244824
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
244821
244825
  res += tmp2 !== void 0 ? tmp2 : "null";
244822
- res += join4;
244826
+ res += join5;
244823
244827
  }
244824
244828
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
244825
244829
  res += tmp !== void 0 ? tmp : "null";
244826
244830
  if (value.length - 1 > maximumBreadth) {
244827
244831
  const removedKeys = value.length - maximumBreadth - 1;
244828
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
244832
+ res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
244829
244833
  }
244830
244834
  if (spacer !== "") {
244831
244835
  res += `
@@ -244846,7 +244850,7 @@ ${originalIndentation}`;
244846
244850
  let separator = "";
244847
244851
  if (spacer !== "") {
244848
244852
  indentation += spacer;
244849
- join4 = `,
244853
+ join5 = `,
244850
244854
  ${indentation}`;
244851
244855
  whitespace = " ";
244852
244856
  }
@@ -244860,13 +244864,13 @@ ${indentation}`;
244860
244864
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
244861
244865
  if (tmp !== void 0) {
244862
244866
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
244863
- separator = join4;
244867
+ separator = join5;
244864
244868
  }
244865
244869
  }
244866
244870
  if (keyLength > maximumBreadth) {
244867
244871
  const removedKeys = keyLength - maximumBreadth;
244868
244872
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
244869
- separator = join4;
244873
+ separator = join5;
244870
244874
  }
244871
244875
  if (spacer !== "" && separator.length > 1) {
244872
244876
  res = `
@@ -244907,7 +244911,7 @@ ${originalIndentation}`;
244907
244911
  }
244908
244912
  const originalIndentation = indentation;
244909
244913
  let res = "";
244910
- let join4 = ",";
244914
+ let join5 = ",";
244911
244915
  if (Array.isArray(value)) {
244912
244916
  if (value.length === 0) {
244913
244917
  return "[]";
@@ -244920,7 +244924,7 @@ ${originalIndentation}`;
244920
244924
  indentation += spacer;
244921
244925
  res += `
244922
244926
  ${indentation}`;
244923
- join4 = `,
244927
+ join5 = `,
244924
244928
  ${indentation}`;
244925
244929
  }
244926
244930
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -244928,13 +244932,13 @@ ${indentation}`;
244928
244932
  for (; i < maximumValuesToStringify - 1; i++) {
244929
244933
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
244930
244934
  res += tmp2 !== void 0 ? tmp2 : "null";
244931
- res += join4;
244935
+ res += join5;
244932
244936
  }
244933
244937
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
244934
244938
  res += tmp !== void 0 ? tmp : "null";
244935
244939
  if (value.length - 1 > maximumBreadth) {
244936
244940
  const removedKeys = value.length - maximumBreadth - 1;
244937
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
244941
+ res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
244938
244942
  }
244939
244943
  if (spacer !== "") {
244940
244944
  res += `
@@ -244947,7 +244951,7 @@ ${originalIndentation}`;
244947
244951
  let whitespace = "";
244948
244952
  if (spacer !== "") {
244949
244953
  indentation += spacer;
244950
- join4 = `,
244954
+ join5 = `,
244951
244955
  ${indentation}`;
244952
244956
  whitespace = " ";
244953
244957
  }
@@ -244956,7 +244960,7 @@ ${indentation}`;
244956
244960
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
244957
244961
  if (tmp !== void 0) {
244958
244962
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
244959
- separator = join4;
244963
+ separator = join5;
244960
244964
  }
244961
244965
  }
244962
244966
  if (spacer !== "" && separator.length > 1) {
@@ -245014,20 +245018,20 @@ ${originalIndentation}`;
245014
245018
  indentation += spacer;
245015
245019
  let res2 = `
245016
245020
  ${indentation}`;
245017
- const join5 = `,
245021
+ const join6 = `,
245018
245022
  ${indentation}`;
245019
245023
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
245020
245024
  let i = 0;
245021
245025
  for (; i < maximumValuesToStringify - 1; i++) {
245022
245026
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
245023
245027
  res2 += tmp2 !== void 0 ? tmp2 : "null";
245024
- res2 += join5;
245028
+ res2 += join6;
245025
245029
  }
245026
245030
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
245027
245031
  res2 += tmp !== void 0 ? tmp : "null";
245028
245032
  if (value.length - 1 > maximumBreadth) {
245029
245033
  const removedKeys = value.length - maximumBreadth - 1;
245030
- res2 += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
245034
+ res2 += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
245031
245035
  }
245032
245036
  res2 += `
245033
245037
  ${originalIndentation}`;
@@ -245043,16 +245047,16 @@ ${originalIndentation}`;
245043
245047
  return '"[Object]"';
245044
245048
  }
245045
245049
  indentation += spacer;
245046
- const join4 = `,
245050
+ const join5 = `,
245047
245051
  ${indentation}`;
245048
245052
  let res = "";
245049
245053
  let separator = "";
245050
245054
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
245051
245055
  if (isTypedArrayWithEntries(value)) {
245052
- res += stringifyTypedArray(value, join4, maximumBreadth);
245056
+ res += stringifyTypedArray(value, join5, maximumBreadth);
245053
245057
  keys = keys.slice(value.length);
245054
245058
  maximumPropertiesToStringify -= value.length;
245055
- separator = join4;
245059
+ separator = join5;
245056
245060
  }
245057
245061
  if (deterministic) {
245058
245062
  keys = sort(keys, comparator);
@@ -245063,13 +245067,13 @@ ${indentation}`;
245063
245067
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
245064
245068
  if (tmp !== void 0) {
245065
245069
  res += `${separator}${strEscape(key2)}: ${tmp}`;
245066
- separator = join4;
245070
+ separator = join5;
245067
245071
  }
245068
245072
  }
245069
245073
  if (keyLength > maximumBreadth) {
245070
245074
  const removedKeys = keyLength - maximumBreadth;
245071
245075
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
245072
- separator = join4;
245076
+ separator = join5;
245073
245077
  }
245074
245078
  if (separator !== "") {
245075
245079
  res = `
@@ -285426,11 +285430,11 @@ var require_core = __commonJS({
285426
285430
  function mapOuter(r, f) {
285427
285431
  return r.matched ? f(r) : r;
285428
285432
  }
285429
- function ab(pa, pb, join4) {
285430
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join4(ma.value, vb, data, i, j)));
285433
+ function ab(pa, pb, join5) {
285434
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join5(ma.value, vb, data, i, j)));
285431
285435
  }
285432
- function abc(pa, pb, pc, join4) {
285433
- return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join4(ma.value, mb.value, vc, data, i, j))));
285436
+ function abc(pa, pb, pc, join5) {
285437
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join5(ma.value, mb.value, vc, data, i, j))));
285434
285438
  }
285435
285439
  function action(f) {
285436
285440
  return (data, i) => {
@@ -312624,7 +312628,7 @@ var require_utils2 = __commonJS({
312624
312628
  module.exports = Utils;
312625
312629
  Utils.prototype.makeDir = function(folder) {
312626
312630
  const self = this;
312627
- function mkdirSync3(fpath) {
312631
+ function mkdirSync4(fpath) {
312628
312632
  let resolvedPath = fpath.split(self.sep)[0];
312629
312633
  fpath.split(self.sep).forEach(function(name) {
312630
312634
  if (!name || name.substr(-1, 1) === ":") return;
@@ -312642,7 +312646,7 @@ var require_utils2 = __commonJS({
312642
312646
  if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
312643
312647
  });
312644
312648
  }
312645
- mkdirSync3(folder);
312649
+ mkdirSync4(folder);
312646
312650
  };
312647
312651
  Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
312648
312652
  const self = this;
@@ -314261,8 +314265,8 @@ var require_adm_zip = __commonJS({
314261
314265
  return null;
314262
314266
  }
314263
314267
  function fixPath(zipPath) {
314264
- const { join: join4, normalize, sep } = pth.posix;
314265
- return join4(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
314268
+ const { join: join5, normalize, sep } = pth.posix;
314269
+ return join5(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
314266
314270
  }
314267
314271
  function filenameFilter(filterfn) {
314268
314272
  if (filterfn instanceof RegExp) {
@@ -319675,14 +319679,14 @@ var require_path_arg = __commonJS({
319675
319679
  var require_find_made = __commonJS({
319676
319680
  "../../node_modules/tar/node_modules/mkdirp/lib/find-made.js"(exports, module) {
319677
319681
  "use strict";
319678
- var { dirname } = __require("path");
319682
+ var { dirname: dirname2 } = __require("path");
319679
319683
  var findMade = (opts, parent, path = void 0) => {
319680
319684
  if (path === parent)
319681
319685
  return Promise.resolve();
319682
319686
  return opts.statAsync(parent).then(
319683
319687
  (st) => st.isDirectory() ? path : void 0,
319684
319688
  // will fail later
319685
- (er) => er.code === "ENOENT" ? findMade(opts, dirname(parent), parent) : void 0
319689
+ (er) => er.code === "ENOENT" ? findMade(opts, dirname2(parent), parent) : void 0
319686
319690
  );
319687
319691
  };
319688
319692
  var findMadeSync = (opts, parent, path = void 0) => {
@@ -319691,7 +319695,7 @@ var require_find_made = __commonJS({
319691
319695
  try {
319692
319696
  return opts.statSync(parent).isDirectory() ? path : void 0;
319693
319697
  } catch (er) {
319694
- return er.code === "ENOENT" ? findMadeSync(opts, dirname(parent), parent) : void 0;
319698
+ return er.code === "ENOENT" ? findMadeSync(opts, dirname2(parent), parent) : void 0;
319695
319699
  }
319696
319700
  };
319697
319701
  module.exports = { findMade, findMadeSync };
@@ -319702,10 +319706,10 @@ var require_find_made = __commonJS({
319702
319706
  var require_mkdirp_manual = __commonJS({
319703
319707
  "../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-manual.js"(exports, module) {
319704
319708
  "use strict";
319705
- var { dirname } = __require("path");
319709
+ var { dirname: dirname2 } = __require("path");
319706
319710
  var mkdirpManual = (path, opts, made) => {
319707
319711
  opts.recursive = false;
319708
- const parent = dirname(path);
319712
+ const parent = dirname2(path);
319709
319713
  if (parent === path) {
319710
319714
  return opts.mkdirAsync(path, opts).catch((er) => {
319711
319715
  if (er.code !== "EISDIR")
@@ -319728,7 +319732,7 @@ var require_mkdirp_manual = __commonJS({
319728
319732
  });
319729
319733
  };
319730
319734
  var mkdirpManualSync = (path, opts, made) => {
319731
- const parent = dirname(path);
319735
+ const parent = dirname2(path);
319732
319736
  opts.recursive = false;
319733
319737
  if (parent === path) {
319734
319738
  try {
@@ -319764,12 +319768,12 @@ var require_mkdirp_manual = __commonJS({
319764
319768
  var require_mkdirp_native = __commonJS({
319765
319769
  "../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-native.js"(exports, module) {
319766
319770
  "use strict";
319767
- var { dirname } = __require("path");
319771
+ var { dirname: dirname2 } = __require("path");
319768
319772
  var { findMade, findMadeSync } = require_find_made();
319769
319773
  var { mkdirpManual, mkdirpManualSync } = require_mkdirp_manual();
319770
319774
  var mkdirpNative = (path, opts) => {
319771
319775
  opts.recursive = true;
319772
- const parent = dirname(path);
319776
+ const parent = dirname2(path);
319773
319777
  if (parent === path)
319774
319778
  return opts.mkdirAsync(path, opts);
319775
319779
  return findMade(opts, path).then((made) => opts.mkdirAsync(path, opts).then(() => made).catch((er) => {
@@ -319781,7 +319785,7 @@ var require_mkdirp_native = __commonJS({
319781
319785
  };
319782
319786
  var mkdirpNativeSync = (path, opts) => {
319783
319787
  opts.recursive = true;
319784
- const parent = dirname(path);
319788
+ const parent = dirname2(path);
319785
319789
  if (parent === path)
319786
319790
  return opts.mkdirSync(path, opts);
319787
319791
  const made = findMadeSync(opts, path);
@@ -320186,7 +320190,7 @@ var require_path_reservations = __commonJS({
320186
320190
  var assert = __require("assert");
320187
320191
  var normalize = require_normalize_unicode();
320188
320192
  var stripSlashes = require_strip_trailing_slashes();
320189
- var { join: join4 } = __require("path");
320193
+ var { join: join5 } = __require("path");
320190
320194
  var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
320191
320195
  var isWindows = platform3 === "win32";
320192
320196
  module.exports = () => {
@@ -320195,7 +320199,7 @@ var require_path_reservations = __commonJS({
320195
320199
  const getDirs = (path) => {
320196
320200
  const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
320197
320201
  if (set.length) {
320198
- path2 = join4(set[set.length - 1], path2);
320202
+ path2 = join5(set[set.length - 1], path2);
320199
320203
  }
320200
320204
  set.push(path2 || "/");
320201
320205
  return set;
@@ -320263,7 +320267,7 @@ var require_path_reservations = __commonJS({
320263
320267
  };
320264
320268
  const reserve = (paths, fn) => {
320265
320269
  paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
320266
- return stripSlashes(join4(normalize(p))).toLowerCase();
320270
+ return stripSlashes(join5(normalize(p))).toLowerCase();
320267
320271
  });
320268
320272
  const dirs = new Set(
320269
320273
  paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
@@ -324801,7 +324805,7 @@ var require_package4 = __commonJS({
324801
324805
  "package.json"(exports, module) {
324802
324806
  module.exports = {
324803
324807
  name: "@validate.qa/runner",
324804
- version: "1.0.2",
324808
+ version: "1.0.4",
324805
324809
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
324806
324810
  bin: {
324807
324811
  "validate-runner": "dist/cli.js"
@@ -324809,7 +324813,7 @@ var require_package4 = __commonJS({
324809
324813
  scripts: {
324810
324814
  build: "tsup",
324811
324815
  dev: "tsx src/cli.ts",
324812
- "test:mobile": "node --import tsx --test src/services/mobile/mobile-network-capture.smoke.test.ts",
324816
+ "test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts",
324813
324817
  prepublishOnly: "npm run build"
324814
324818
  },
324815
324819
  files: [
@@ -324887,9 +324891,9 @@ function getConfigDir() {
324887
324891
  }
324888
324892
 
324889
324893
  // src/runner.ts
324890
- import { existsSync as existsSync2, statSync, mkdirSync as mkdirSync2 } from "fs";
324891
- import { join as join3 } from "path";
324892
- import { homedir as homedir2 } from "os";
324894
+ import { existsSync as existsSync3, statSync, mkdirSync as mkdirSync3 } from "fs";
324895
+ import { join as join4 } from "path";
324896
+ import { homedir as homedir3 } from "os";
324893
324897
  import { createHash } from "crypto";
324894
324898
 
324895
324899
  // src/services/playwright-native.ts
@@ -325302,11 +325306,12 @@ function discoverRealIOSDevices() {
325302
325306
  for (const [index, node] of iosNodes.entries()) {
325303
325307
  const serial = typeof node.serial_num === "string" && node.serial_num.trim() ? node.serial_num.trim() : `usb-ios-device-${index + 1}`;
325304
325308
  const name = typeof node._name === "string" && node._name.trim() ? node._name.trim() : "iOS Device";
325309
+ const version = runCommand("ideviceinfo", ["-u", serial, "-k", "ProductVersion"]) || "unknown";
325305
325310
  devices.push({
325306
325311
  id: serial,
325307
325312
  name,
325308
325313
  platform: "IOS",
325309
- platformVersion: "unknown",
325314
+ platformVersion: version,
325310
325315
  state: "BOOTED",
325311
325316
  isAvailable: true,
325312
325317
  isPhysical: true
@@ -325381,7 +325386,7 @@ function parseAdbDevices() {
325381
325386
  const output = runCommand("adb", ["devices", "-l"]);
325382
325387
  if (!output) return [];
325383
325388
  const lines = output.split("\n").slice(1);
325384
- return lines.filter((line) => line.includes(" ") || line.includes("device ")).map((line) => {
325389
+ return lines.filter((line) => line.trim().length > 0).map((line) => {
325385
325390
  const parts = line.trim().split(/\s+/);
325386
325391
  const serial = parts[0];
325387
325392
  const type = parts[1] ?? "";
@@ -325397,26 +325402,25 @@ function parseAdbDevices() {
325397
325402
  device: props["device"],
325398
325403
  transportId: props["transport_id"]
325399
325404
  };
325400
- }).filter((d) => d.type === "device");
325405
+ }).filter((d) => d.type === "device" || d.type === "unauthorized");
325401
325406
  }
325402
325407
  function getAndroidProp(serial, prop) {
325403
325408
  return runCommand("adb", ["-s", serial, "shell", "getprop", prop]);
325404
325409
  }
325405
325410
  function discoverAndroidDevices() {
325406
325411
  const adbDevices = parseAdbDevices();
325407
- return adbDevices.filter((d) => {
325408
- return d.type === "device";
325409
- }).map((d) => {
325412
+ return adbDevices.filter((d) => d.type === "device" || d.type === "unauthorized").map((d) => {
325410
325413
  const isEmulator = d.serial.startsWith("emulator-");
325414
+ const isAvailable = d.type === "device";
325411
325415
  const name = d.model || d.device || (isEmulator ? "Android Emulator" : "Android Device");
325412
- const platformVersion = getAndroidProp(d.serial, "ro.build.version.release") || "unknown";
325416
+ const platformVersion = isAvailable ? getAndroidProp(d.serial, "ro.build.version.release") || "unknown" : "unknown";
325413
325417
  return {
325414
325418
  id: d.serial,
325415
325419
  name,
325416
325420
  platform: "ANDROID",
325417
325421
  platformVersion,
325418
325422
  state: "BOOTED",
325419
- isAvailable: true,
325423
+ isAvailable,
325420
325424
  // Mark if it's a real device vs emulator
325421
325425
  ...isEmulator ? {} : { isPhysical: true }
325422
325426
  };
@@ -325449,10 +325453,14 @@ function discoverAllDevices(options) {
325449
325453
  const androidDevices = discoverAndroidDevices();
325450
325454
  if (options?.includeApps) {
325451
325455
  for (const device of iosDevices) {
325452
- device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
325456
+ if (device.isAvailable) {
325457
+ device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
325458
+ }
325453
325459
  }
325454
325460
  for (const device of androidDevices) {
325455
- device.installedApps = listAndroidInstalledApps(device.id);
325461
+ if (device.isAvailable) {
325462
+ device.installedApps = listAndroidInstalledApps(device.id);
325463
+ }
325456
325464
  }
325457
325465
  }
325458
325466
  return [...iosDevices, ...androidDevices];
@@ -325460,11 +325468,12 @@ function discoverAllDevices(options) {
325460
325468
  function getRunnerCapabilities() {
325461
325469
  const iosDevices = discoverIOSDevices();
325462
325470
  const androidDevices = discoverAndroidDevices();
325471
+ const isReadyDevice = (device) => device.isAvailable && device.state === "BOOTED";
325463
325472
  return {
325464
325473
  web: true,
325465
325474
  // Always capable of web testing via Playwright
325466
- ios: iosDevices.length > 0,
325467
- android: androidDevices.length > 0
325475
+ ios: iosDevices.some(isReadyDevice),
325476
+ android: androidDevices.some(isReadyDevice)
325468
325477
  };
325469
325478
  }
325470
325479
 
@@ -325866,7 +325875,7 @@ async function executeMobileTest(options) {
325866
325875
  const devices = discoverAllDevices({ includeApps: true });
325867
325876
  const platformFilter = options.target.platform;
325868
325877
  const matchingDevices = devices.filter(
325869
- (d) => d.platform === platformFilter && d.state === "BOOTED"
325878
+ (d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
325870
325879
  );
325871
325880
  if (matchingDevices.length === 0) {
325872
325881
  throw new Error(
@@ -326030,7 +326039,7 @@ async function createMobileDiscoverySession(target) {
326030
326039
  const devices = discoverAllDevices({ includeApps: true });
326031
326040
  const platformFilter = target.platform;
326032
326041
  const matchingDevices = devices.filter(
326033
- (d) => d.platform === platformFilter && d.state === "BOOTED"
326042
+ (d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
326034
326043
  );
326035
326044
  if (matchingDevices.length === 0) {
326036
326045
  throw new Error(
@@ -326081,6 +326090,9 @@ async function createMobileDiscoverySession(target) {
326081
326090
  import { createServer } from "http";
326082
326091
  import { execFileSync as execFileSync3 } from "child_process";
326083
326092
  import { randomBytes, timingSafeEqual } from "crypto";
326093
+ import { homedir as homedir2 } from "os";
326094
+ import { join as join2, dirname } from "path";
326095
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync2 } from "fs";
326084
326096
  init_dist();
326085
326097
 
326086
326098
  // src/services/mobile/appium-actions.ts
@@ -326311,6 +326323,7 @@ var polling = false;
326311
326323
  var sessionStartInFlight = null;
326312
326324
  var sessionReaperTimer = null;
326313
326325
  var mirrorClientsEmptyAt = null;
326326
+ var mirrorSessionOwner = null;
326314
326327
  var executorBusy = false;
326315
326328
  var rateLimitBuckets = /* @__PURE__ */ new Map();
326316
326329
  function isRateLimited(clientIp) {
@@ -326366,30 +326379,50 @@ async function getWindowSize3() {
326366
326379
  return getWindowSize2(`/session/${state.appiumSessionId}`);
326367
326380
  }
326368
326381
  var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
326369
- function killPort2(port) {
326370
- if (!Number.isInteger(port) || port <= 0 || port > 65535) return;
326382
+ var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
326383
+ var BRIDGE_PID_FILE = join2(homedir2(), ".validate.qa", "mobile-bridge.pid");
326384
+ function readBridgePidFile() {
326371
326385
  try {
326372
- const pids = execFileSync3("lsof", ["-ti", `:${port}`], { encoding: "utf-8" }).trim();
326373
- if (pids) {
326374
- const pidList = pids.split("\n").filter(Boolean);
326375
- for (const pid of pidList) {
326376
- if (!/^\d+$/.test(pid)) continue;
326377
- let command = "";
326378
- try {
326379
- command = execFileSync3("ps", ["-p", pid, "-o", "command="], { encoding: "utf-8" }).trim();
326380
- } catch {
326381
- continue;
326382
- }
326383
- if (!command.includes(BRIDGE_PROCESS_MARKER)) {
326384
- continue;
326385
- }
326386
- try {
326387
- execFileSync3("kill", ["-9", pid], { stdio: "ignore" });
326388
- } catch {
326389
- }
326390
- }
326386
+ const raw = readFileSync2(BRIDGE_PID_FILE, "utf-8").trim();
326387
+ const pid = parseInt(raw, 10);
326388
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
326389
+ } catch {
326390
+ return null;
326391
+ }
326392
+ }
326393
+ function writeBridgePidFile(pid) {
326394
+ try {
326395
+ mkdirSync2(dirname(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
326396
+ writeFileSync2(BRIDGE_PID_FILE, String(pid), { mode: 384 });
326397
+ } catch {
326398
+ }
326399
+ }
326400
+ function clearBridgePidFile() {
326401
+ try {
326402
+ unlinkSync2(BRIDGE_PID_FILE);
326403
+ } catch {
326404
+ }
326405
+ }
326406
+ function killStaleBridge() {
326407
+ const pid = readBridgePidFile();
326408
+ if (!pid) return;
326409
+ try {
326410
+ execFileSync3("kill", ["-0", String(pid)], { stdio: "ignore" });
326411
+ } catch {
326412
+ clearBridgePidFile();
326413
+ return;
326414
+ }
326415
+ try {
326416
+ const pidsOnPort = execFileSync3("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
326417
+ const pidList = pidsOnPort.split("\n").filter(Boolean);
326418
+ if (!pidList.includes(String(pid))) {
326419
+ return;
326391
326420
  }
326421
+ execFileSync3("kill", ["-9", String(pid)], { stdio: "ignore" });
326422
+ execFileSync3("sleep", ["1"], { stdio: "ignore" });
326392
326423
  } catch {
326424
+ } finally {
326425
+ clearBridgePidFile();
326393
326426
  }
326394
326427
  }
326395
326428
  function parseBody(req) {
@@ -326627,6 +326660,7 @@ async function startSession(validated, res) {
326627
326660
  }
326628
326661
  state.appiumSessionId = null;
326629
326662
  state.platform = null;
326663
+ mirrorSessionOwner = null;
326630
326664
  }
326631
326665
  stopAllMirrorClients();
326632
326666
  const realCaps = isPhysicalDevice(deviceId, targetPlatform) ? realDeviceCapabilities2(targetPlatform) : {};
@@ -326657,11 +326691,14 @@ async function startSession(validated, res) {
326657
326691
  })
326658
326692
  });
326659
326693
  state.appiumSessionId = result?.value?.sessionId ?? result?.sessionId ?? null;
326660
- state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
326661
326694
  if (!state.appiumSessionId) {
326695
+ state.platform = null;
326696
+ mirrorSessionOwner = null;
326662
326697
  sendJSON(res, 500, { error: "Appium session creation returned no session ID" });
326663
326698
  return;
326664
326699
  }
326700
+ state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
326701
+ mirrorSessionOwner = "recording";
326665
326702
  if (launchMode === "DEEP_LINK" && deeplink) {
326666
326703
  try {
326667
326704
  const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
@@ -326686,6 +326723,7 @@ async function handleSessionStop(_req, res) {
326686
326723
  }
326687
326724
  state.appiumSessionId = null;
326688
326725
  state.platform = null;
326726
+ mirrorSessionOwner = null;
326689
326727
  }
326690
326728
  stopAllMirrorClients();
326691
326729
  stopSessionReaper();
@@ -326766,6 +326804,7 @@ async function pollAndBroadcastFrame() {
326766
326804
  if (!alive) {
326767
326805
  state.appiumSessionId = null;
326768
326806
  state.platform = null;
326807
+ mirrorSessionOwner = null;
326769
326808
  stopAllMirrorClients();
326770
326809
  return;
326771
326810
  }
@@ -326836,6 +326875,7 @@ async function reapIdleSession() {
326836
326875
  const sessionId = state.appiumSessionId;
326837
326876
  state.appiumSessionId = null;
326838
326877
  state.platform = null;
326878
+ mirrorSessionOwner = null;
326839
326879
  mirrorClientsEmptyAt = null;
326840
326880
  stopSessionReaper();
326841
326881
  try {
@@ -327014,7 +327054,7 @@ async function startMobileBridge(options) {
327014
327054
  if (!process.title.includes(BRIDGE_PROCESS_MARKER)) {
327015
327055
  process.title = `${process.title} ${BRIDGE_PROCESS_MARKER}`;
327016
327056
  }
327017
- killPort2(port);
327057
+ killStaleBridge();
327018
327058
  return new Promise((resolve, reject) => {
327019
327059
  server2 = createServer(route);
327020
327060
  server2.on("error", (err) => {
@@ -327030,6 +327070,7 @@ async function startMobileBridge(options) {
327030
327070
  state.port = port;
327031
327071
  state.tunnelToken = tunnelToken;
327032
327072
  state.tunnelUrl = void 0;
327073
+ writeBridgePidFile(process.pid);
327033
327074
  let tunnelUrl;
327034
327075
  if (enableTunnel) {
327035
327076
  try {
@@ -327097,6 +327138,7 @@ async function startMobileBridge(options) {
327097
327138
  let checkInterval = null;
327098
327139
  const timeout = setTimeout(() => {
327099
327140
  if (checkInterval) clearInterval(checkInterval);
327141
+ checkInterval = null;
327100
327142
  if (!tunnelUrl) {
327101
327143
  reject2(new Error("ngrok tunnel failed to start"));
327102
327144
  }
@@ -327105,6 +327147,7 @@ async function startMobileBridge(options) {
327105
327147
  if (tunnelUrl) {
327106
327148
  clearTimeout(timeout);
327107
327149
  if (checkInterval) clearInterval(checkInterval);
327150
+ checkInterval = null;
327108
327151
  resolve2();
327109
327152
  }
327110
327153
  }, 500);
@@ -327134,6 +327177,7 @@ async function startMobileBridge(options) {
327134
327177
  async function stopMobileBridge() {
327135
327178
  stopAllMirrorClients();
327136
327179
  stopSessionReaper();
327180
+ clearBridgePidFile();
327137
327181
  if (state.appiumSessionId) {
327138
327182
  try {
327139
327183
  validateSessionId(state.appiumSessionId);
@@ -327142,6 +327186,7 @@ async function stopMobileBridge() {
327142
327186
  }
327143
327187
  state.appiumSessionId = null;
327144
327188
  state.platform = null;
327189
+ mirrorSessionOwner = null;
327145
327190
  }
327146
327191
  if (ngrokProcess) {
327147
327192
  try {
@@ -327172,13 +327217,37 @@ function getBridgeState() {
327172
327217
  tunnelUrl: state.tunnelUrl,
327173
327218
  tunnelToken: state.tunnelToken,
327174
327219
  hasActiveSession: !!state.appiumSessionId,
327175
- recordingActive: !!state.appiumSessionId,
327220
+ recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording",
327176
327221
  platform: state.platform
327177
327222
  };
327178
327223
  }
327179
327224
  function setExecutorBusy(busy) {
327180
327225
  executorBusy = busy;
327181
327226
  }
327227
+ function attachMirrorSession(sessionId, platform3) {
327228
+ validateSessionId(sessionId);
327229
+ if (state.appiumSessionId && state.appiumSessionId !== sessionId) {
327230
+ stopAllMirrorClients();
327231
+ }
327232
+ state.appiumSessionId = sessionId;
327233
+ state.platform = platform3;
327234
+ mirrorSessionOwner = "runner";
327235
+ mirrorNullTicks = 0;
327236
+ mirrorClientsEmptyAt = null;
327237
+ ensureSessionReaper();
327238
+ if (state.mirrorClients.size > 0) {
327239
+ ensureMirrorPolling();
327240
+ void pollAndBroadcastFrame();
327241
+ }
327242
+ }
327243
+ function detachMirrorSession(sessionId) {
327244
+ if (sessionId && state.appiumSessionId && state.appiumSessionId !== sessionId) return;
327245
+ state.appiumSessionId = null;
327246
+ state.platform = null;
327247
+ mirrorSessionOwner = null;
327248
+ stopAllMirrorClients();
327249
+ stopSessionReaper();
327250
+ }
327182
327251
 
327183
327252
  // src/services/mobile/mobile-bridge-driver.ts
327184
327253
  var import_runner_core7 = __toESM(require_dist2());
@@ -327392,7 +327461,7 @@ import { execFile, execFileSync as execFileSync4 } from "child_process";
327392
327461
  import { randomUUID } from "crypto";
327393
327462
  import { mkdtemp, rm, writeFile, readFile, readdir, mkdir, unlink } from "fs/promises";
327394
327463
  import { tmpdir, networkInterfaces } from "os";
327395
- import { join as join2 } from "path";
327464
+ import { join as join3 } from "path";
327396
327465
  import { promisify } from "util";
327397
327466
  import {
327398
327467
  getLocal,
@@ -327404,7 +327473,7 @@ var MAX_BODY_CAPTURE_BYTES = 64 * 1024;
327404
327473
  var DEVICE_CMD_TIMEOUT_MS = 15e3;
327405
327474
  var CA_KEY_BITS = 2048;
327406
327475
  var ANDROID_EMULATOR_HOST_LOOPBACK = "10.0.2.2";
327407
- var PENDING_CLEANUP_DIR = join2(tmpdir(), "validateqa-mobile-capture-pending");
327476
+ var PENDING_CLEANUP_DIR = join3(tmpdir(), "validateqa-mobile-capture-pending");
327408
327477
  function rawHeadersToPairs(rawHeaders) {
327409
327478
  return rawHeaders.map(([name, value]) => ({ name, value }));
327410
327479
  }
@@ -327688,7 +327757,7 @@ async function generateCaCert(dir, onLog) {
327688
327757
  // rely on stop() to remove the cert from the device promptly after the run
327689
327758
  // rather than on a short expiry.
327690
327759
  });
327691
- const caCertPath = join2(dir, "validateqa-mobile-ca.pem");
327760
+ const caCertPath = join3(dir, "validateqa-mobile-ca.pem");
327692
327761
  await writeFile(caCertPath, cert, "utf-8");
327693
327762
  return { caCertPath, caKeyPem: key, caCertPem: cert };
327694
327763
  } catch (err) {
@@ -327794,7 +327863,7 @@ function errMsg(err) {
327794
327863
  var activeCleanups = /* @__PURE__ */ new Map();
327795
327864
  var exitHandlersInstalled = false;
327796
327865
  function markerPath(sessionKey) {
327797
- return join2(PENDING_CLEANUP_DIR, `${sessionKey}.json`);
327866
+ return join3(PENDING_CLEANUP_DIR, `${sessionKey}.json`);
327798
327867
  }
327799
327868
  async function writePendingCleanup(sessionKey, marker) {
327800
327869
  try {
@@ -327836,7 +327905,7 @@ async function reconcilePendingCleanups(onLog) {
327836
327905
  }
327837
327906
  for (const file of files) {
327838
327907
  if (!file.endsWith(".json")) continue;
327839
- const full = join2(PENDING_CLEANUP_DIR, file);
327908
+ const full = join3(PENDING_CLEANUP_DIR, file);
327840
327909
  try {
327841
327910
  const raw = await readFile(full, "utf-8");
327842
327911
  const marker = JSON.parse(raw);
@@ -327881,7 +327950,7 @@ async function startMobileNetworkCapture(options) {
327881
327950
  let caKeyPem = null;
327882
327951
  let caCertPem = null;
327883
327952
  try {
327884
- tmpDir = await mkdtemp(join2(tmpdir(), "validateqa-mobile-ca-"));
327953
+ tmpDir = await mkdtemp(join3(tmpdir(), "validateqa-mobile-ca-"));
327885
327954
  const ca = await generateCaCert(tmpDir, onLog);
327886
327955
  if (ca) {
327887
327956
  caCertPath = ca.caCertPath;
@@ -327997,6 +328066,219 @@ async function startMobileNetworkCapture(options) {
327997
328066
  };
327998
328067
  }
327999
328068
 
328069
+ // src/services/doctor.ts
328070
+ import { execSync } from "child_process";
328071
+ import { existsSync as existsSync2 } from "fs";
328072
+ import { platform as platform2 } from "os";
328073
+ var C = {
328074
+ reset: "\x1B[0m",
328075
+ bold: "\x1B[1m",
328076
+ green: "\x1B[32m",
328077
+ red: "\x1B[31m",
328078
+ yellow: "\x1B[33m",
328079
+ cyan: "\x1B[36m",
328080
+ gray: "\x1B[90m"
328081
+ };
328082
+ function run(cmd) {
328083
+ try {
328084
+ return execSync(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
328085
+ } catch {
328086
+ return null;
328087
+ }
328088
+ }
328089
+ function check(name, fn) {
328090
+ try {
328091
+ const result = fn();
328092
+ return { name, ...result };
328093
+ } catch (err) {
328094
+ return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
328095
+ }
328096
+ }
328097
+ function checkWeb() {
328098
+ const results = [];
328099
+ results.push(check("Node.js", () => {
328100
+ const version = process.version;
328101
+ const major = parseInt(version.slice(1).split(".")[0], 10);
328102
+ return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
328103
+ }));
328104
+ results.push(check("Playwright", () => {
328105
+ const version = run("npx playwright --version");
328106
+ if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
328107
+ return { ok: true, detail: version };
328108
+ }));
328109
+ return results;
328110
+ }
328111
+ function checkIOS() {
328112
+ const results = [];
328113
+ const isMac = platform2() === "darwin";
328114
+ results.push(check("macOS", () => ({
328115
+ ok: isMac,
328116
+ detail: isMac ? `${platform2()} detected` : "iOS testing requires macOS"
328117
+ })));
328118
+ if (!isMac) return results;
328119
+ results.push(check("Xcode", () => {
328120
+ const version = run("xcodebuild -version");
328121
+ if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
328122
+ const firstLine = version.split("\n")[0];
328123
+ return { ok: true, detail: firstLine };
328124
+ }));
328125
+ results.push(check("Xcode Command Line Tools", () => {
328126
+ const path = run("xcode-select -p");
328127
+ return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
328128
+ }));
328129
+ results.push(check("Appium xcuitest driver", () => {
328130
+ const drivers = run("appium driver list --installed --json");
328131
+ if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
328132
+ try {
328133
+ const parsed = JSON.parse(drivers);
328134
+ const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
328135
+ return {
328136
+ ok: !!hasXcuitest,
328137
+ detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
328138
+ };
328139
+ } catch {
328140
+ const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
328141
+ return {
328142
+ ok: hasXcuitest,
328143
+ detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
328144
+ };
328145
+ }
328146
+ }));
328147
+ results.push(check("iOS Devices", () => {
328148
+ const json = run("xcrun simctl list devices booted --json");
328149
+ let bootedSimulators = 0;
328150
+ if (json) {
328151
+ try {
328152
+ const parsed = JSON.parse(json);
328153
+ const devices = Object.values(parsed.devices ?? {}).flat();
328154
+ bootedSimulators = devices.filter((d) => d.state === "Booted").length;
328155
+ } catch {
328156
+ }
328157
+ }
328158
+ const realDevices = discoverRealIOSDevices().length;
328159
+ const total = bootedSimulators + realDevices;
328160
+ if (total === 0) {
328161
+ return {
328162
+ ok: false,
328163
+ detail: "No devices found. Boot a simulator or connect a real device via USB."
328164
+ };
328165
+ }
328166
+ const details = [];
328167
+ if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
328168
+ if (realDevices > 0) details.push(`${realDevices} real device(s)`);
328169
+ return {
328170
+ ok: true,
328171
+ detail: `${total} device(s) available: ${details.join(", ")}`
328172
+ };
328173
+ }));
328174
+ return results;
328175
+ }
328176
+ function checkAndroid() {
328177
+ const results = [];
328178
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
328179
+ results.push(check("ANDROID_HOME", () => ({
328180
+ ok: !!androidHome && existsSync2(androidHome),
328181
+ detail: androidHome ? existsSync2(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
328182
+ })));
328183
+ if (!androidHome || !existsSync2(androidHome)) return results;
328184
+ results.push(check("adb", () => {
328185
+ const version = run("adb version");
328186
+ if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
328187
+ const firstLine = version.split("\n")[0];
328188
+ return { ok: true, detail: firstLine };
328189
+ }));
328190
+ results.push(check("Appium uiautomator2 driver", () => {
328191
+ const drivers = run("appium driver list --installed --json");
328192
+ if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
328193
+ try {
328194
+ const parsed = JSON.parse(drivers);
328195
+ const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
328196
+ return {
328197
+ ok: !!hasUia2,
328198
+ detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
328199
+ };
328200
+ } catch {
328201
+ const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
328202
+ return {
328203
+ ok: hasUia2,
328204
+ detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
328205
+ };
328206
+ }
328207
+ }));
328208
+ results.push(check("Android Devices", () => {
328209
+ const output = run("adb devices");
328210
+ if (!output) return { ok: false, detail: "adb devices failed" };
328211
+ const lines = output.split("\n").filter((l) => l.includes(" device"));
328212
+ const emulators = lines.filter((l) => l.startsWith("emulator-"));
328213
+ const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
328214
+ const total = emulators.length + realDevices.length;
328215
+ if (total === 0) {
328216
+ return {
328217
+ ok: false,
328218
+ detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
328219
+ };
328220
+ }
328221
+ const details = [];
328222
+ if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
328223
+ if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
328224
+ return {
328225
+ ok: true,
328226
+ detail: `${total} device(s) available: ${details.join(", ")}`
328227
+ };
328228
+ }));
328229
+ return results;
328230
+ }
328231
+ function checkTunnel() {
328232
+ const version = run("ngrok version") || run("ngrok --version");
328233
+ if (!version) {
328234
+ return [{
328235
+ name: "ngrok",
328236
+ ok: false,
328237
+ detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
328238
+ }];
328239
+ }
328240
+ const match = version.match(/(\d+)\.\d+\.\d+/);
328241
+ const major = match ? parseInt(match[1], 10) : 0;
328242
+ return [{
328243
+ name: "ngrok",
328244
+ ok: major >= 3,
328245
+ detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
328246
+ }];
328247
+ }
328248
+ function runDoctor() {
328249
+ const web = checkWeb();
328250
+ const ios = checkIOS();
328251
+ const android = checkAndroid();
328252
+ const tunnel = checkTunnel();
328253
+ const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
328254
+ return { web, ios, android, tunnel, allPassed };
328255
+ }
328256
+ function printDoctorReport(report) {
328257
+ console.log("");
328258
+ console.log(`${C.bold}${C.cyan}\u{1F3E5} validate.qa Doctor${C.reset}`);
328259
+ console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
328260
+ const printSection = (title, checks) => {
328261
+ console.log("");
328262
+ console.log(` ${C.bold}${title}:${C.reset}`);
328263
+ for (const c of checks) {
328264
+ const icon = c.ok ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`;
328265
+ console.log(` ${icon} ${c.name}: ${c.detail}`);
328266
+ }
328267
+ };
328268
+ printSection("Web Testing", report.web);
328269
+ printSection("iOS Testing", report.ios);
328270
+ printSection("Android Testing", report.android);
328271
+ printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
328272
+ console.log("");
328273
+ if (report.allPassed) {
328274
+ console.log(` ${C.green}\u2705 All checks passed - ready for web + mobile testing${C.reset}`);
328275
+ } else {
328276
+ const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
328277
+ console.log(` ${C.yellow}\u26A0 ${failCount} check(s) need attention${C.reset}`);
328278
+ }
328279
+ console.log("");
328280
+ }
328281
+
328000
328282
  // src/runner.ts
328001
328283
  init_dist();
328002
328284
  var import_runner_core11 = __toESM(require_dist2());
@@ -328086,7 +328368,7 @@ var browserPool = new import_runner_core6.BrowserPool({
328086
328368
  maxContextsPerBrowser: envInt("MAX_CONTEXTS_PER_BROWSER", DEFAULT_MAX_CONTEXTS_PER_BROWSER),
328087
328369
  retireAfterContexts: 30
328088
328370
  });
328089
- var C = {
328371
+ var C2 = {
328090
328372
  reset: "\x1B[0m",
328091
328373
  bold: "\x1B[1m",
328092
328374
  green: "\x1B[32m",
@@ -328096,24 +328378,24 @@ var C = {
328096
328378
  gray: "\x1B[90m"
328097
328379
  };
328098
328380
  var log = {
328099
- info: (msg) => console.log(`${C.cyan}\u2139${C.reset} ${msg}`),
328100
- ok: (msg) => console.log(`${C.green}\u2713${C.reset} ${msg}`),
328101
- fail: (msg) => console.log(`${C.red}\u2717${C.reset} ${msg}`),
328102
- warn: (msg) => console.log(`${C.yellow}\u26A0${C.reset} ${msg}`),
328103
- dim: (msg) => console.log(`${C.gray}${msg}${C.reset}`)
328381
+ info: (msg) => console.log(`${C2.cyan}\u2139${C2.reset} ${msg}`),
328382
+ ok: (msg) => console.log(`${C2.green}\u2713${C2.reset} ${msg}`),
328383
+ fail: (msg) => console.log(`${C2.red}\u2717${C2.reset} ${msg}`),
328384
+ warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
328385
+ dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
328104
328386
  };
328105
328387
  var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
328106
328388
  function getRunsDir() {
328107
- const dir = process.env.RUNS_DIR || join3(homedir2(), ".validate.qa", "runs");
328108
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
328389
+ const dir = process.env.RUNS_DIR || join4(homedir3(), ".validate.qa", "runs");
328390
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
328109
328391
  return dir;
328110
328392
  }
328111
328393
  function authStatePath(projectId) {
328112
- return join3(getRunsDir(), `auth-state-${projectId || "default"}.json`);
328394
+ return join4(getRunsDir(), `auth-state-${projectId || "default"}.json`);
328113
328395
  }
328114
328396
  function getValidAuthState(projectId) {
328115
328397
  const path = authStatePath(projectId);
328116
- if (!existsSync2(path)) return void 0;
328398
+ if (!existsSync3(path)) return void 0;
328117
328399
  const age = Date.now() - statSync(path).mtimeMs;
328118
328400
  return age < AUTH_MAX_AGE_MS ? path : void 0;
328119
328401
  }
@@ -329056,9 +329338,9 @@ ${finalVerifyResult.logs ?? ""}`;
329056
329338
  (l) => l.includes("Phase 1: Script-driven") || l.includes("Phase 2: Escalating") || l.includes("Phase 2: Script + MCP") || l.includes("Phase 1 succeeded") || l.includes("Phase 2 iteration") || l.includes("\u{1F916} Verdict:") || l.includes("\u{1F916} finish_fix:") || l.includes("native verification PASSED") || l.includes("native verification FAILED") || l.includes("Native run PASSED") || l.includes("Native run FAILED") || l.includes("APP_BUG") || l.includes("CONFIG_ISSUE") || l.includes("needs_browser") || l.includes("--- Attempt") || l.includes("Proven commands") || l.includes("Proven fix:") || l.includes("Same error repeated") || l.includes("aborting heal")
329057
329339
  );
329058
329340
  if (phaseLines.length > 0) {
329059
- console.log(`${C.gray}\u2500\u2500 Heal summary \u2500\u2500${C.reset}`);
329341
+ console.log(`${C2.gray}\u2500\u2500 Heal summary \u2500\u2500${C2.reset}`);
329060
329342
  for (const line of phaseLines) console.log(line);
329061
- console.log(`${C.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C.reset}`);
329343
+ console.log(`${C2.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C2.reset}`);
329062
329344
  }
329063
329345
  }
329064
329346
  log[result2.passed ? "ok" : "fail"](
@@ -329597,11 +329879,18 @@ ${finalVerifyResult.logs ?? ""}`;
329597
329879
  }
329598
329880
  try {
329599
329881
  if (isNativeMobile) {
329600
- const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : "ANDROID";
329601
329882
  const mobileTarget = ctx.target;
329602
329883
  if (!mobileTarget || !mobileTarget.appId) {
329603
329884
  throw new Error("Native mobile discovery requires a target with an appId (bundleId/appPackage).");
329604
329885
  }
329886
+ const targetPlatform = mobileTarget.platform;
329887
+ const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : mobileMedium === "ANDROID_NATIVE" ? "ANDROID" : targetPlatform === "IOS" || targetPlatform === "ANDROID" ? targetPlatform : null;
329888
+ if (!platform3) {
329889
+ throw new Error("Native mobile discovery requires medium IOS_NATIVE/ANDROID_NATIVE or target.platform IOS/ANDROID.");
329890
+ }
329891
+ if (mobileMedium === "IOS_NATIVE" && targetPlatform === "ANDROID" || mobileMedium === "ANDROID_NATIVE" && targetPlatform === "IOS") {
329892
+ throw new Error("Native mobile discovery medium does not match target.platform.");
329893
+ }
329605
329894
  if (getBridgeState().hasActiveSession) {
329606
329895
  throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
329607
329896
  }
@@ -329620,6 +329909,7 @@ ${finalVerifyResult.logs ?? ""}`;
329620
329909
  deeplink: mobileTarget.deeplink
329621
329910
  });
329622
329911
  mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
329912
+ attachMirrorSession(session.sessionId, platform3);
329623
329913
  const driver = new MobileBridgeDriver({
329624
329914
  sessionId: session.sessionId,
329625
329915
  platform: platform3,
@@ -329660,6 +329950,7 @@ ${finalVerifyResult.logs ?? ""}`;
329660
329950
  }
329661
329951
  }
329662
329952
  if (session) {
329953
+ detachMirrorSession(session.sessionId);
329663
329954
  try {
329664
329955
  await session.stop();
329665
329956
  } catch {
@@ -330030,9 +330321,9 @@ ${finalVerifyResult.logs ?? ""}`;
330030
330321
  } else {
330031
330322
  log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
330032
330323
  if (result.logs) {
330033
- console.log(`${C.gray}\u2500\u2500 Runner logs \u2500\u2500${C.reset}`);
330324
+ console.log(`${C2.gray}\u2500\u2500 Runner logs \u2500\u2500${C2.reset}`);
330034
330325
  console.log(result.logs);
330035
- console.log(`${C.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C.reset}`);
330326
+ console.log(`${C2.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C2.reset}`);
330036
330327
  }
330037
330328
  }
330038
330329
  } catch (err) {
@@ -330054,20 +330345,24 @@ ${finalVerifyResult.logs ?? ""}`;
330054
330345
  }
330055
330346
  async function startRunner(opts) {
330056
330347
  const { serverUrl, authToken, projectId, pollIntervalMs = 2e3, runnerMode = "playwright-native", enableMobile = false, enableMobileTunnel = false } = opts;
330348
+ process.env.SERVER_URL = serverUrl;
330349
+ process.env.AUTH_TOKEN = authToken;
330350
+ process.env.RUNNER_TOKEN = authToken;
330351
+ if (projectId) process.env.PROJECT_ID = projectId;
330057
330352
  const headers = buildHeaders(authToken, projectId);
330058
330353
  console.log("");
330059
- console.log(`${C.bold}${C.cyan}\u{1F399}\uFE0F validate.qa Local Runner${C.reset}`);
330060
- console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
330354
+ console.log(`${C2.bold}${C2.cyan}\u{1F399}\uFE0F validate.qa Local Runner${C2.reset}`);
330355
+ console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
330061
330356
  log.info(`Server: ${serverUrl}`);
330062
330357
  log.info(`Project: ${projectId}`);
330063
330358
  log.info(`Mode: \u{1F3AD} Playwright Native${enableMobile ? " + \u{1F4F1} Mobile" : ""}${enableMobile && enableMobileTunnel ? " \u{1F310} Tunnel" : ""}`);
330064
330359
  log.info(`Interval: ${pollIntervalMs}ms`);
330065
330360
  if (process.env.DISCOVERY_PROMPT_V3 === "true") {
330066
- log.ok("Discovery: V3 forced (DISCOVERY_PROMPT_V3=true)");
330361
+ log.ok("Discovery prompt: V3 (per-turn rebuild) \u2014 DISCOVERY_PROMPT_V3=true");
330067
330362
  } else if (process.env.DISCOVERY_PROMPT_V3 === "false") {
330068
- log.ok("Discovery: V2.5 forced (DISCOVERY_PROMPT_V3=false)");
330363
+ log.ok("Discovery prompt: V2.5 (full transcript + control brief) \u2014 DISCOVERY_PROMPT_V3=false");
330069
330364
  } else {
330070
- log.ok("Discovery: auto mode (Grok -> V2.5, others -> V3)");
330365
+ log.ok("Discovery prompt: V3 (per-turn rebuild, default) \u2014 set DISCOVERY_PROMPT_V3=false for V2.5");
330071
330366
  }
330072
330367
  if ((0, import_runner_core5.hasUsableChromeProfile)()) {
330073
330368
  log.ok(`Browser: Chrome with user profile (cookies/logins persist)`);
@@ -330079,8 +330374,8 @@ async function startRunner(opts) {
330079
330374
  const devices = discoverAllDevices();
330080
330375
  const capabilities = {
330081
330376
  web: true,
330082
- ios: devices.some((d) => d.platform === "IOS"),
330083
- android: devices.some((d) => d.platform === "ANDROID")
330377
+ ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
330378
+ android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
330084
330379
  };
330085
330380
  log.info(`\u{1F4F1} Mobile: iOS ${capabilities.ios ? "\u2713" : "\u2717"} | Android ${capabilities.android ? "\u2713" : "\u2717"} | ${devices.length} device(s)`);
330086
330381
  try {
@@ -330141,6 +330436,33 @@ async function startRunner(opts) {
330141
330436
  const startedAt = Date.now();
330142
330437
  const MAX_BACKOFF_MS = 6e4;
330143
330438
  let consecutivePollErrors = 0;
330439
+ let cachedDoctor = null;
330440
+ const DOCTOR_TTL_MS = 5 * 60 * 1e3;
330441
+ let doctorRefreshInFlight = false;
330442
+ const refreshDoctorInBackground = () => {
330443
+ if (doctorRefreshInFlight) return;
330444
+ doctorRefreshInFlight = true;
330445
+ setImmediate(() => {
330446
+ try {
330447
+ const report = runDoctor();
330448
+ cachedDoctor = { report, at: Date.now() };
330449
+ } catch (err) {
330450
+ log.dim(` [doctor] refresh failed: ${err instanceof Error ? err.message : String(err)}`);
330451
+ } finally {
330452
+ doctorRefreshInFlight = false;
330453
+ }
330454
+ });
330455
+ };
330456
+ const getCachedDoctor = () => {
330457
+ if (!enableMobile) return void 0;
330458
+ if (!cachedDoctor) {
330459
+ refreshDoctorInBackground();
330460
+ return void 0;
330461
+ }
330462
+ const now = Date.now();
330463
+ if (now - cachedDoctor.at >= DOCTOR_TTL_MS) refreshDoctorInBackground();
330464
+ return cachedDoctor.report;
330465
+ };
330144
330466
  log.info(`Concurrency: ${RESOURCE_GOVERNOR_OPTIONS.adaptive ? `adaptive (${RESOURCE_GOVERNOR_OPTIONS.maxUnits} weighted units, hard cap ${MAX_CONCURRENT})` : `static (hard cap ${MAX_CONCURRENT})`}`);
330145
330467
  const poolSize = envInt("BROWSER_POOL_SIZE", DEFAULT_BROWSER_POOL_SIZE);
330146
330468
  if (poolSize > 0) {
@@ -330249,7 +330571,11 @@ async function startRunner(opts) {
330249
330571
  const sendHeartbeat = async () => {
330250
330572
  try {
330251
330573
  const devices = enableMobile ? discoverAllDevices() : [];
330252
- const capabilities = enableMobile ? { web: true, ios: devices.some((d) => d.platform === "IOS"), android: devices.some((d) => d.platform === "ANDROID") } : { web: true, ios: false, android: false };
330574
+ const capabilities = enableMobile ? {
330575
+ web: true,
330576
+ ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
330577
+ android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
330578
+ } : { web: true, ios: false, android: false };
330253
330579
  const bridge = enableMobile ? getBridgeState() : void 0;
330254
330580
  const resourceStatus = (0, import_runner_core11.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
330255
330581
  const processInventory = await (0, import_runner_core11.getBrowserProcessInventory)();
@@ -330280,7 +330606,10 @@ async function startRunner(opts) {
330280
330606
  resourceGovernor: resourceStatus.governor,
330281
330607
  processInventory,
330282
330608
  aiQueue: (0, import_runner_core11.getRunnerAIQueueStats)(),
330283
- liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat()
330609
+ liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat(),
330610
+ // Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
330611
+ // Cached & cheap — see getCachedDoctor above.
330612
+ doctor: getCachedDoctor()
330284
330613
  }),
330285
330614
  signal: AbortSignal.timeout(1e4)
330286
330615
  });
@@ -330480,219 +330809,6 @@ async function startRunner(opts) {
330480
330809
  }
330481
330810
  }
330482
330811
 
330483
- // src/services/doctor.ts
330484
- import { execSync } from "child_process";
330485
- import { existsSync as existsSync3 } from "fs";
330486
- import { platform as platform2 } from "os";
330487
- var C2 = {
330488
- reset: "\x1B[0m",
330489
- bold: "\x1B[1m",
330490
- green: "\x1B[32m",
330491
- red: "\x1B[31m",
330492
- yellow: "\x1B[33m",
330493
- cyan: "\x1B[36m",
330494
- gray: "\x1B[90m"
330495
- };
330496
- function run(cmd) {
330497
- try {
330498
- return execSync(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
330499
- } catch {
330500
- return null;
330501
- }
330502
- }
330503
- function check(name, fn) {
330504
- try {
330505
- const result = fn();
330506
- return { name, ...result };
330507
- } catch (err) {
330508
- return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
330509
- }
330510
- }
330511
- function checkWeb() {
330512
- const results = [];
330513
- results.push(check("Node.js", () => {
330514
- const version = process.version;
330515
- const major = parseInt(version.slice(1).split(".")[0], 10);
330516
- return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
330517
- }));
330518
- results.push(check("Playwright", () => {
330519
- const version = run("npx playwright --version");
330520
- if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
330521
- return { ok: true, detail: version };
330522
- }));
330523
- return results;
330524
- }
330525
- function checkIOS() {
330526
- const results = [];
330527
- const isMac = platform2() === "darwin";
330528
- results.push(check("macOS", () => ({
330529
- ok: isMac,
330530
- detail: isMac ? `${platform2()} detected` : "iOS testing requires macOS"
330531
- })));
330532
- if (!isMac) return results;
330533
- results.push(check("Xcode", () => {
330534
- const version = run("xcodebuild -version");
330535
- if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
330536
- const firstLine = version.split("\n")[0];
330537
- return { ok: true, detail: firstLine };
330538
- }));
330539
- results.push(check("Xcode Command Line Tools", () => {
330540
- const path = run("xcode-select -p");
330541
- return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
330542
- }));
330543
- results.push(check("Appium xcuitest driver", () => {
330544
- const drivers = run("appium driver list --installed --json");
330545
- if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
330546
- try {
330547
- const parsed = JSON.parse(drivers);
330548
- const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
330549
- return {
330550
- ok: !!hasXcuitest,
330551
- detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
330552
- };
330553
- } catch {
330554
- const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
330555
- return {
330556
- ok: hasXcuitest,
330557
- detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
330558
- };
330559
- }
330560
- }));
330561
- results.push(check("iOS Devices", () => {
330562
- const json = run("xcrun simctl list devices booted --json");
330563
- let bootedSimulators = 0;
330564
- if (json) {
330565
- try {
330566
- const parsed = JSON.parse(json);
330567
- const devices = Object.values(parsed.devices ?? {}).flat();
330568
- bootedSimulators = devices.filter((d) => d.state === "Booted").length;
330569
- } catch {
330570
- }
330571
- }
330572
- const realDevices = discoverRealIOSDevices().length;
330573
- const total = bootedSimulators + realDevices;
330574
- if (total === 0) {
330575
- return {
330576
- ok: false,
330577
- detail: "No devices found. Boot a simulator or connect a real device via USB."
330578
- };
330579
- }
330580
- const details = [];
330581
- if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
330582
- if (realDevices > 0) details.push(`${realDevices} real device(s)`);
330583
- return {
330584
- ok: true,
330585
- detail: `${total} device(s) available: ${details.join(", ")}`
330586
- };
330587
- }));
330588
- return results;
330589
- }
330590
- function checkAndroid() {
330591
- const results = [];
330592
- const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
330593
- results.push(check("ANDROID_HOME", () => ({
330594
- ok: !!androidHome && existsSync3(androidHome),
330595
- detail: androidHome ? existsSync3(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
330596
- })));
330597
- if (!androidHome || !existsSync3(androidHome)) return results;
330598
- results.push(check("adb", () => {
330599
- const version = run("adb version");
330600
- if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
330601
- const firstLine = version.split("\n")[0];
330602
- return { ok: true, detail: firstLine };
330603
- }));
330604
- results.push(check("Appium uiautomator2 driver", () => {
330605
- const drivers = run("appium driver list --installed --json");
330606
- if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
330607
- try {
330608
- const parsed = JSON.parse(drivers);
330609
- const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
330610
- return {
330611
- ok: !!hasUia2,
330612
- detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
330613
- };
330614
- } catch {
330615
- const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
330616
- return {
330617
- ok: hasUia2,
330618
- detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
330619
- };
330620
- }
330621
- }));
330622
- results.push(check("Android Devices", () => {
330623
- const output = run("adb devices");
330624
- if (!output) return { ok: false, detail: "adb devices failed" };
330625
- const lines = output.split("\n").filter((l) => l.includes(" device"));
330626
- const emulators = lines.filter((l) => l.startsWith("emulator-"));
330627
- const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
330628
- const total = emulators.length + realDevices.length;
330629
- if (total === 0) {
330630
- return {
330631
- ok: false,
330632
- detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
330633
- };
330634
- }
330635
- const details = [];
330636
- if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
330637
- if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
330638
- return {
330639
- ok: true,
330640
- detail: `${total} device(s) available: ${details.join(", ")}`
330641
- };
330642
- }));
330643
- return results;
330644
- }
330645
- function checkTunnel() {
330646
- const version = run("ngrok version") || run("ngrok --version");
330647
- if (!version) {
330648
- return [{
330649
- name: "ngrok",
330650
- ok: false,
330651
- detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
330652
- }];
330653
- }
330654
- const match = version.match(/(\d+)\.\d+\.\d+/);
330655
- const major = match ? parseInt(match[1], 10) : 0;
330656
- return [{
330657
- name: "ngrok",
330658
- ok: major >= 3,
330659
- detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
330660
- }];
330661
- }
330662
- function runDoctor() {
330663
- const web = checkWeb();
330664
- const ios = checkIOS();
330665
- const android = checkAndroid();
330666
- const tunnel = checkTunnel();
330667
- const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
330668
- return { web, ios, android, tunnel, allPassed };
330669
- }
330670
- function printDoctorReport(report) {
330671
- console.log("");
330672
- console.log(`${C2.bold}${C2.cyan}\u{1F3E5} validate.qa Doctor${C2.reset}`);
330673
- console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
330674
- const printSection = (title, checks) => {
330675
- console.log("");
330676
- console.log(` ${C2.bold}${title}:${C2.reset}`);
330677
- for (const c of checks) {
330678
- const icon = c.ok ? `${C2.green}\u2713${C2.reset}` : `${C2.red}\u2717${C2.reset}`;
330679
- console.log(` ${icon} ${c.name}: ${c.detail}`);
330680
- }
330681
- };
330682
- printSection("Web Testing", report.web);
330683
- printSection("iOS Testing", report.ios);
330684
- printSection("Android Testing", report.android);
330685
- printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
330686
- console.log("");
330687
- if (report.allPassed) {
330688
- console.log(` ${C2.green}\u2705 All checks passed - ready for web + mobile testing${C2.reset}`);
330689
- } else {
330690
- const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
330691
- console.log(` ${C2.yellow}\u26A0 ${failCount} check(s) need attention${C2.reset}`);
330692
- }
330693
- console.log("");
330694
- }
330695
-
330696
330812
  // src/cli.ts
330697
330813
  var pkg = require_package4();
330698
330814
  var program = new Command();