@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.js CHANGED
@@ -1405,7 +1405,7 @@ function selectorForValue(value) {
1405
1405
  if (value.startsWith("~") || value.startsWith("id="))
1406
1406
  return value;
1407
1407
  if (value.includes(":id/"))
1408
- return `id=${value.split(":id/")[1]}`;
1408
+ return `id=${value}`;
1409
1409
  return `~${value}`;
1410
1410
  }
1411
1411
  function selectorCandidatesForStep(step) {
@@ -7552,7 +7552,7 @@ Each entry is exactly one of:
7552
7552
  const systemPrompt = context.mode === "survey" ? SURVEY_SYSTEM_PROMPT : exports2.SCENARIO_SYSTEM_PROMPT;
7553
7553
  const userPrompt = formatEvidenceForPlanner(compressed, evidence, context);
7554
7554
  logs2.push(`AI planner: ~${Math.ceil(userPrompt.length / 3.5)} input tokens`);
7555
- const aiClient = options.modelOverride ? (0, ai_provider_js_1.getClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getAIClient)();
7555
+ const aiClient = options.aiClient ?? (options.modelOverride ? (0, ai_provider_js_1.getClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getAIClient)());
7556
7556
  const messages = [
7557
7557
  { role: "system", content: systemPrompt },
7558
7558
  { role: "user", content: userPrompt }
@@ -7764,6 +7764,7 @@ var require_mobile_discovery_prompts = __commonJS({
7764
7764
  exports2.formatMobileEvidenceForPlanner = formatMobileEvidenceForPlanner;
7765
7765
  exports2.buildMobileSiteMap = buildMobileSiteMap;
7766
7766
  exports2.runMobileAIPlanner = runMobileAIPlanner;
7767
+ var ai_provider_js_1 = require_ai_provider();
7767
7768
  var discover_ai_planner_js_1 = require_discover_ai_planner();
7768
7769
  exports2.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.
7769
7770
 
@@ -8163,6 +8164,8 @@ ${constraints.join("\n")}`);
8163
8164
  }
8164
8165
  async function runMobileAIPlanner(input, options = {}) {
8165
8166
  const planner = options.planner ?? discover_ai_planner_js_1.runAIPlanner;
8167
+ const shouldBuildMobileAIClient = (process.env.SERVER_URL ?? "").trim().length > 0 || !options.planner;
8168
+ const aiClient = options.aiClient ?? (shouldBuildMobileAIClient ? options.modelOverride ? (0, ai_provider_js_1.getMobileClientForModel)(options.modelOverride) : (0, ai_provider_js_1.getMobileAIClient)() : void 0);
8166
8169
  const siteMap = buildMobileSiteMap(input.graph, {
8167
8170
  projectId: input.projectId,
8168
8171
  appId: input.appId
@@ -8188,7 +8191,8 @@ ${constraints.join("\n")}`);
8188
8191
  testBudget: input.testBudget
8189
8192
  }, {
8190
8193
  modelOverride: options.modelOverride,
8191
- onAICall: options.onAICall
8194
+ onAICall: options.onAICall,
8195
+ ...aiClient ? { aiClient } : {}
8192
8196
  });
8193
8197
  }
8194
8198
  }
@@ -21738,7 +21742,7 @@ var require_util = __commonJS({
21738
21742
  return path;
21739
21743
  }
21740
21744
  exports2.normalize = normalize;
21741
- function join4(aRoot, aPath) {
21745
+ function join5(aRoot, aPath) {
21742
21746
  if (aRoot === "") {
21743
21747
  aRoot = ".";
21744
21748
  }
@@ -21770,7 +21774,7 @@ var require_util = __commonJS({
21770
21774
  }
21771
21775
  return joined;
21772
21776
  }
21773
- exports2.join = join4;
21777
+ exports2.join = join5;
21774
21778
  exports2.isAbsolute = function(aPath) {
21775
21779
  return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
21776
21780
  };
@@ -21943,7 +21947,7 @@ var require_util = __commonJS({
21943
21947
  parsed.path = parsed.path.substring(0, index + 1);
21944
21948
  }
21945
21949
  }
21946
- sourceURL = join4(urlGenerate(parsed), sourceURL);
21950
+ sourceURL = join5(urlGenerate(parsed), sourceURL);
21947
21951
  }
21948
21952
  return normalize(sourceURL);
21949
21953
  }
@@ -243098,7 +243102,7 @@ var require_thread_stream = __commonJS({
243098
243102
  var { version } = require_package();
243099
243103
  var { EventEmitter: EventEmitter2 } = require("events");
243100
243104
  var { Worker } = require("worker_threads");
243101
- var { join: join4 } = require("path");
243105
+ var { join: join5 } = require("path");
243102
243106
  var { pathToFileURL } = require("url");
243103
243107
  var { wait } = require_wait();
243104
243108
  var {
@@ -243149,7 +243153,7 @@ var require_thread_stream = __commonJS({
243149
243153
  function createWorker(stream, opts) {
243150
243154
  const { filename, workerData } = opts;
243151
243155
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
243152
- const toExecute = bundlerOverrides["thread-stream-worker"] || join4(__dirname, "lib", "worker.js");
243156
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join5(__dirname, "lib", "worker.js");
243153
243157
  const worker = new Worker(toExecute, {
243154
243158
  ...opts.workerOpts,
243155
243159
  name: opts.workerOpts?.name || "thread-stream",
@@ -243617,7 +243621,7 @@ var require_transport = __commonJS({
243617
243621
  var { createRequire } = require("module");
243618
243622
  var { existsSync: existsSync4 } = require("fs");
243619
243623
  var getCallers = require_caller();
243620
- var { join: join4, isAbsolute, sep } = require("path");
243624
+ var { join: join5, isAbsolute, sep } = require("path");
243621
243625
  var { fileURLToPath } = require("url");
243622
243626
  var sleep = require_atomic_sleep();
243623
243627
  var onExit = require_on_exit_leak_free();
@@ -243770,7 +243774,7 @@ var require_transport = __commonJS({
243770
243774
  throw new Error("only one of target or targets can be specified");
243771
243775
  }
243772
243776
  if (targets) {
243773
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
243777
+ target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
243774
243778
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
243775
243779
  return {
243776
243780
  ...dest,
@@ -243788,7 +243792,7 @@ var require_transport = __commonJS({
243788
243792
  });
243789
243793
  });
243790
243794
  } else if (pipeline) {
243791
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
243795
+ target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
243792
243796
  options.pipelines = [pipeline.map((dest) => {
243793
243797
  return {
243794
243798
  ...dest,
@@ -243811,7 +243815,7 @@ var require_transport = __commonJS({
243811
243815
  return origin;
243812
243816
  }
243813
243817
  if (origin === "pino/file") {
243814
- return join4(__dirname, "..", "file.js");
243818
+ return join5(__dirname, "..", "file.js");
243815
243819
  }
243816
243820
  let fixTarget2;
243817
243821
  for (const filePath of callers) {
@@ -244792,7 +244796,7 @@ var require_safe_stable_stringify = __commonJS({
244792
244796
  return circularValue;
244793
244797
  }
244794
244798
  let res = "";
244795
- let join4 = ",";
244799
+ let join5 = ",";
244796
244800
  const originalIndentation = indentation;
244797
244801
  if (Array.isArray(value)) {
244798
244802
  if (value.length === 0) {
@@ -244806,7 +244810,7 @@ var require_safe_stable_stringify = __commonJS({
244806
244810
  indentation += spacer;
244807
244811
  res += `
244808
244812
  ${indentation}`;
244809
- join4 = `,
244813
+ join5 = `,
244810
244814
  ${indentation}`;
244811
244815
  }
244812
244816
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -244814,13 +244818,13 @@ ${indentation}`;
244814
244818
  for (; i < maximumValuesToStringify - 1; i++) {
244815
244819
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
244816
244820
  res += tmp2 !== void 0 ? tmp2 : "null";
244817
- res += join4;
244821
+ res += join5;
244818
244822
  }
244819
244823
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
244820
244824
  res += tmp !== void 0 ? tmp : "null";
244821
244825
  if (value.length - 1 > maximumBreadth) {
244822
244826
  const removedKeys = value.length - maximumBreadth - 1;
244823
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
244827
+ res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
244824
244828
  }
244825
244829
  if (spacer !== "") {
244826
244830
  res += `
@@ -244841,7 +244845,7 @@ ${originalIndentation}`;
244841
244845
  let separator = "";
244842
244846
  if (spacer !== "") {
244843
244847
  indentation += spacer;
244844
- join4 = `,
244848
+ join5 = `,
244845
244849
  ${indentation}`;
244846
244850
  whitespace = " ";
244847
244851
  }
@@ -244855,13 +244859,13 @@ ${indentation}`;
244855
244859
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
244856
244860
  if (tmp !== void 0) {
244857
244861
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
244858
- separator = join4;
244862
+ separator = join5;
244859
244863
  }
244860
244864
  }
244861
244865
  if (keyLength > maximumBreadth) {
244862
244866
  const removedKeys = keyLength - maximumBreadth;
244863
244867
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
244864
- separator = join4;
244868
+ separator = join5;
244865
244869
  }
244866
244870
  if (spacer !== "" && separator.length > 1) {
244867
244871
  res = `
@@ -244902,7 +244906,7 @@ ${originalIndentation}`;
244902
244906
  }
244903
244907
  const originalIndentation = indentation;
244904
244908
  let res = "";
244905
- let join4 = ",";
244909
+ let join5 = ",";
244906
244910
  if (Array.isArray(value)) {
244907
244911
  if (value.length === 0) {
244908
244912
  return "[]";
@@ -244915,7 +244919,7 @@ ${originalIndentation}`;
244915
244919
  indentation += spacer;
244916
244920
  res += `
244917
244921
  ${indentation}`;
244918
- join4 = `,
244922
+ join5 = `,
244919
244923
  ${indentation}`;
244920
244924
  }
244921
244925
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -244923,13 +244927,13 @@ ${indentation}`;
244923
244927
  for (; i < maximumValuesToStringify - 1; i++) {
244924
244928
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
244925
244929
  res += tmp2 !== void 0 ? tmp2 : "null";
244926
- res += join4;
244930
+ res += join5;
244927
244931
  }
244928
244932
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
244929
244933
  res += tmp !== void 0 ? tmp : "null";
244930
244934
  if (value.length - 1 > maximumBreadth) {
244931
244935
  const removedKeys = value.length - maximumBreadth - 1;
244932
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
244936
+ res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
244933
244937
  }
244934
244938
  if (spacer !== "") {
244935
244939
  res += `
@@ -244942,7 +244946,7 @@ ${originalIndentation}`;
244942
244946
  let whitespace = "";
244943
244947
  if (spacer !== "") {
244944
244948
  indentation += spacer;
244945
- join4 = `,
244949
+ join5 = `,
244946
244950
  ${indentation}`;
244947
244951
  whitespace = " ";
244948
244952
  }
@@ -244951,7 +244955,7 @@ ${indentation}`;
244951
244955
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
244952
244956
  if (tmp !== void 0) {
244953
244957
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
244954
- separator = join4;
244958
+ separator = join5;
244955
244959
  }
244956
244960
  }
244957
244961
  if (spacer !== "" && separator.length > 1) {
@@ -245009,20 +245013,20 @@ ${originalIndentation}`;
245009
245013
  indentation += spacer;
245010
245014
  let res2 = `
245011
245015
  ${indentation}`;
245012
- const join5 = `,
245016
+ const join6 = `,
245013
245017
  ${indentation}`;
245014
245018
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
245015
245019
  let i = 0;
245016
245020
  for (; i < maximumValuesToStringify - 1; i++) {
245017
245021
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
245018
245022
  res2 += tmp2 !== void 0 ? tmp2 : "null";
245019
- res2 += join5;
245023
+ res2 += join6;
245020
245024
  }
245021
245025
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
245022
245026
  res2 += tmp !== void 0 ? tmp : "null";
245023
245027
  if (value.length - 1 > maximumBreadth) {
245024
245028
  const removedKeys = value.length - maximumBreadth - 1;
245025
- res2 += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
245029
+ res2 += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
245026
245030
  }
245027
245031
  res2 += `
245028
245032
  ${originalIndentation}`;
@@ -245038,16 +245042,16 @@ ${originalIndentation}`;
245038
245042
  return '"[Object]"';
245039
245043
  }
245040
245044
  indentation += spacer;
245041
- const join4 = `,
245045
+ const join5 = `,
245042
245046
  ${indentation}`;
245043
245047
  let res = "";
245044
245048
  let separator = "";
245045
245049
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
245046
245050
  if (isTypedArrayWithEntries(value)) {
245047
- res += stringifyTypedArray(value, join4, maximumBreadth);
245051
+ res += stringifyTypedArray(value, join5, maximumBreadth);
245048
245052
  keys = keys.slice(value.length);
245049
245053
  maximumPropertiesToStringify -= value.length;
245050
- separator = join4;
245054
+ separator = join5;
245051
245055
  }
245052
245056
  if (deterministic) {
245053
245057
  keys = sort(keys, comparator);
@@ -245058,13 +245062,13 @@ ${indentation}`;
245058
245062
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
245059
245063
  if (tmp !== void 0) {
245060
245064
  res += `${separator}${strEscape(key2)}: ${tmp}`;
245061
- separator = join4;
245065
+ separator = join5;
245062
245066
  }
245063
245067
  }
245064
245068
  if (keyLength > maximumBreadth) {
245065
245069
  const removedKeys = keyLength - maximumBreadth;
245066
245070
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
245067
- separator = join4;
245071
+ separator = join5;
245068
245072
  }
245069
245073
  if (separator !== "") {
245070
245074
  res = `
@@ -285421,11 +285425,11 @@ var require_core = __commonJS({
285421
285425
  function mapOuter(r, f) {
285422
285426
  return r.matched ? f(r) : r;
285423
285427
  }
285424
- function ab(pa, pb, join4) {
285425
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join4(ma.value, vb, data, i, j)));
285428
+ function ab(pa, pb, join5) {
285429
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join5(ma.value, vb, data, i, j)));
285426
285430
  }
285427
- function abc(pa, pb, pc, join4) {
285428
- 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))));
285431
+ function abc(pa, pb, pc, join5) {
285432
+ 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))));
285429
285433
  }
285430
285434
  function action(f) {
285431
285435
  return (data, i) => {
@@ -312619,7 +312623,7 @@ var require_utils2 = __commonJS({
312619
312623
  module2.exports = Utils;
312620
312624
  Utils.prototype.makeDir = function(folder) {
312621
312625
  const self = this;
312622
- function mkdirSync3(fpath) {
312626
+ function mkdirSync4(fpath) {
312623
312627
  let resolvedPath = fpath.split(self.sep)[0];
312624
312628
  fpath.split(self.sep).forEach(function(name) {
312625
312629
  if (!name || name.substr(-1, 1) === ":") return;
@@ -312637,7 +312641,7 @@ var require_utils2 = __commonJS({
312637
312641
  if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
312638
312642
  });
312639
312643
  }
312640
- mkdirSync3(folder);
312644
+ mkdirSync4(folder);
312641
312645
  };
312642
312646
  Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
312643
312647
  const self = this;
@@ -314256,8 +314260,8 @@ var require_adm_zip = __commonJS({
314256
314260
  return null;
314257
314261
  }
314258
314262
  function fixPath(zipPath) {
314259
- const { join: join4, normalize, sep } = pth.posix;
314260
- return join4(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
314263
+ const { join: join5, normalize, sep } = pth.posix;
314264
+ return join5(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
314261
314265
  }
314262
314266
  function filenameFilter(filterfn) {
314263
314267
  if (filterfn instanceof RegExp) {
@@ -319670,14 +319674,14 @@ var require_path_arg = __commonJS({
319670
319674
  var require_find_made = __commonJS({
319671
319675
  "../../node_modules/tar/node_modules/mkdirp/lib/find-made.js"(exports2, module2) {
319672
319676
  "use strict";
319673
- var { dirname } = require("path");
319677
+ var { dirname: dirname2 } = require("path");
319674
319678
  var findMade = (opts, parent, path = void 0) => {
319675
319679
  if (path === parent)
319676
319680
  return Promise.resolve();
319677
319681
  return opts.statAsync(parent).then(
319678
319682
  (st) => st.isDirectory() ? path : void 0,
319679
319683
  // will fail later
319680
- (er) => er.code === "ENOENT" ? findMade(opts, dirname(parent), parent) : void 0
319684
+ (er) => er.code === "ENOENT" ? findMade(opts, dirname2(parent), parent) : void 0
319681
319685
  );
319682
319686
  };
319683
319687
  var findMadeSync = (opts, parent, path = void 0) => {
@@ -319686,7 +319690,7 @@ var require_find_made = __commonJS({
319686
319690
  try {
319687
319691
  return opts.statSync(parent).isDirectory() ? path : void 0;
319688
319692
  } catch (er) {
319689
- return er.code === "ENOENT" ? findMadeSync(opts, dirname(parent), parent) : void 0;
319693
+ return er.code === "ENOENT" ? findMadeSync(opts, dirname2(parent), parent) : void 0;
319690
319694
  }
319691
319695
  };
319692
319696
  module2.exports = { findMade, findMadeSync };
@@ -319697,10 +319701,10 @@ var require_find_made = __commonJS({
319697
319701
  var require_mkdirp_manual = __commonJS({
319698
319702
  "../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-manual.js"(exports2, module2) {
319699
319703
  "use strict";
319700
- var { dirname } = require("path");
319704
+ var { dirname: dirname2 } = require("path");
319701
319705
  var mkdirpManual = (path, opts, made) => {
319702
319706
  opts.recursive = false;
319703
- const parent = dirname(path);
319707
+ const parent = dirname2(path);
319704
319708
  if (parent === path) {
319705
319709
  return opts.mkdirAsync(path, opts).catch((er) => {
319706
319710
  if (er.code !== "EISDIR")
@@ -319723,7 +319727,7 @@ var require_mkdirp_manual = __commonJS({
319723
319727
  });
319724
319728
  };
319725
319729
  var mkdirpManualSync = (path, opts, made) => {
319726
- const parent = dirname(path);
319730
+ const parent = dirname2(path);
319727
319731
  opts.recursive = false;
319728
319732
  if (parent === path) {
319729
319733
  try {
@@ -319759,12 +319763,12 @@ var require_mkdirp_manual = __commonJS({
319759
319763
  var require_mkdirp_native = __commonJS({
319760
319764
  "../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-native.js"(exports2, module2) {
319761
319765
  "use strict";
319762
- var { dirname } = require("path");
319766
+ var { dirname: dirname2 } = require("path");
319763
319767
  var { findMade, findMadeSync } = require_find_made();
319764
319768
  var { mkdirpManual, mkdirpManualSync } = require_mkdirp_manual();
319765
319769
  var mkdirpNative = (path, opts) => {
319766
319770
  opts.recursive = true;
319767
- const parent = dirname(path);
319771
+ const parent = dirname2(path);
319768
319772
  if (parent === path)
319769
319773
  return opts.mkdirAsync(path, opts);
319770
319774
  return findMade(opts, path).then((made) => opts.mkdirAsync(path, opts).then(() => made).catch((er) => {
@@ -319776,7 +319780,7 @@ var require_mkdirp_native = __commonJS({
319776
319780
  };
319777
319781
  var mkdirpNativeSync = (path, opts) => {
319778
319782
  opts.recursive = true;
319779
- const parent = dirname(path);
319783
+ const parent = dirname2(path);
319780
319784
  if (parent === path)
319781
319785
  return opts.mkdirSync(path, opts);
319782
319786
  const made = findMadeSync(opts, path);
@@ -320181,7 +320185,7 @@ var require_path_reservations = __commonJS({
320181
320185
  var assert = require("assert");
320182
320186
  var normalize = require_normalize_unicode();
320183
320187
  var stripSlashes = require_strip_trailing_slashes();
320184
- var { join: join4 } = require("path");
320188
+ var { join: join5 } = require("path");
320185
320189
  var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
320186
320190
  var isWindows = platform3 === "win32";
320187
320191
  module2.exports = () => {
@@ -320190,7 +320194,7 @@ var require_path_reservations = __commonJS({
320190
320194
  const getDirs = (path) => {
320191
320195
  const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
320192
320196
  if (set.length) {
320193
- path2 = join4(set[set.length - 1], path2);
320197
+ path2 = join5(set[set.length - 1], path2);
320194
320198
  }
320195
320199
  set.push(path2 || "/");
320196
320200
  return set;
@@ -320258,7 +320262,7 @@ var require_path_reservations = __commonJS({
320258
320262
  };
320259
320263
  const reserve = (paths, fn) => {
320260
320264
  paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
320261
- return stripSlashes(join4(normalize(p))).toLowerCase();
320265
+ return stripSlashes(join5(normalize(p))).toLowerCase();
320262
320266
  });
320263
320267
  const dirs = new Set(
320264
320268
  paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
@@ -324796,7 +324800,7 @@ var require_package4 = __commonJS({
324796
324800
  "package.json"(exports2, module2) {
324797
324801
  module2.exports = {
324798
324802
  name: "@validate.qa/runner",
324799
- version: "1.0.2",
324803
+ version: "1.0.4",
324800
324804
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
324801
324805
  bin: {
324802
324806
  "validate-runner": "dist/cli.js"
@@ -324804,7 +324808,7 @@ var require_package4 = __commonJS({
324804
324808
  scripts: {
324805
324809
  build: "tsup",
324806
324810
  dev: "tsx src/cli.ts",
324807
- "test:mobile": "node --import tsx --test src/services/mobile/mobile-network-capture.smoke.test.ts",
324811
+ "test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts",
324808
324812
  prepublishOnly: "npm run build"
324809
324813
  },
324810
324814
  files: [
@@ -324882,9 +324886,9 @@ function getConfigDir() {
324882
324886
  }
324883
324887
 
324884
324888
  // src/runner.ts
324885
- var import_fs2 = require("fs");
324886
- var import_path2 = require("path");
324887
- var import_os3 = require("os");
324889
+ var import_fs4 = require("fs");
324890
+ var import_path3 = require("path");
324891
+ var import_os5 = require("os");
324888
324892
  var import_node_crypto2 = require("crypto");
324889
324893
 
324890
324894
  // src/services/playwright-native.ts
@@ -325297,11 +325301,12 @@ function discoverRealIOSDevices() {
325297
325301
  for (const [index, node] of iosNodes.entries()) {
325298
325302
  const serial = typeof node.serial_num === "string" && node.serial_num.trim() ? node.serial_num.trim() : `usb-ios-device-${index + 1}`;
325299
325303
  const name = typeof node._name === "string" && node._name.trim() ? node._name.trim() : "iOS Device";
325304
+ const version = runCommand("ideviceinfo", ["-u", serial, "-k", "ProductVersion"]) || "unknown";
325300
325305
  devices.push({
325301
325306
  id: serial,
325302
325307
  name,
325303
325308
  platform: "IOS",
325304
- platformVersion: "unknown",
325309
+ platformVersion: version,
325305
325310
  state: "BOOTED",
325306
325311
  isAvailable: true,
325307
325312
  isPhysical: true
@@ -325376,7 +325381,7 @@ function parseAdbDevices() {
325376
325381
  const output = runCommand("adb", ["devices", "-l"]);
325377
325382
  if (!output) return [];
325378
325383
  const lines = output.split("\n").slice(1);
325379
- return lines.filter((line) => line.includes(" ") || line.includes("device ")).map((line) => {
325384
+ return lines.filter((line) => line.trim().length > 0).map((line) => {
325380
325385
  const parts = line.trim().split(/\s+/);
325381
325386
  const serial = parts[0];
325382
325387
  const type = parts[1] ?? "";
@@ -325392,26 +325397,25 @@ function parseAdbDevices() {
325392
325397
  device: props["device"],
325393
325398
  transportId: props["transport_id"]
325394
325399
  };
325395
- }).filter((d) => d.type === "device");
325400
+ }).filter((d) => d.type === "device" || d.type === "unauthorized");
325396
325401
  }
325397
325402
  function getAndroidProp(serial, prop) {
325398
325403
  return runCommand("adb", ["-s", serial, "shell", "getprop", prop]);
325399
325404
  }
325400
325405
  function discoverAndroidDevices() {
325401
325406
  const adbDevices = parseAdbDevices();
325402
- return adbDevices.filter((d) => {
325403
- return d.type === "device";
325404
- }).map((d) => {
325407
+ return adbDevices.filter((d) => d.type === "device" || d.type === "unauthorized").map((d) => {
325405
325408
  const isEmulator = d.serial.startsWith("emulator-");
325409
+ const isAvailable = d.type === "device";
325406
325410
  const name = d.model || d.device || (isEmulator ? "Android Emulator" : "Android Device");
325407
- const platformVersion = getAndroidProp(d.serial, "ro.build.version.release") || "unknown";
325411
+ const platformVersion = isAvailable ? getAndroidProp(d.serial, "ro.build.version.release") || "unknown" : "unknown";
325408
325412
  return {
325409
325413
  id: d.serial,
325410
325414
  name,
325411
325415
  platform: "ANDROID",
325412
325416
  platformVersion,
325413
325417
  state: "BOOTED",
325414
- isAvailable: true,
325418
+ isAvailable,
325415
325419
  // Mark if it's a real device vs emulator
325416
325420
  ...isEmulator ? {} : { isPhysical: true }
325417
325421
  };
@@ -325444,10 +325448,14 @@ function discoverAllDevices(options) {
325444
325448
  const androidDevices = discoverAndroidDevices();
325445
325449
  if (options?.includeApps) {
325446
325450
  for (const device of iosDevices) {
325447
- device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
325451
+ if (device.isAvailable) {
325452
+ device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
325453
+ }
325448
325454
  }
325449
325455
  for (const device of androidDevices) {
325450
- device.installedApps = listAndroidInstalledApps(device.id);
325456
+ if (device.isAvailable) {
325457
+ device.installedApps = listAndroidInstalledApps(device.id);
325458
+ }
325451
325459
  }
325452
325460
  }
325453
325461
  return [...iosDevices, ...androidDevices];
@@ -325455,11 +325463,12 @@ function discoverAllDevices(options) {
325455
325463
  function getRunnerCapabilities() {
325456
325464
  const iosDevices = discoverIOSDevices();
325457
325465
  const androidDevices = discoverAndroidDevices();
325466
+ const isReadyDevice = (device) => device.isAvailable && device.state === "BOOTED";
325458
325467
  return {
325459
325468
  web: true,
325460
325469
  // Always capable of web testing via Playwright
325461
- ios: iosDevices.length > 0,
325462
- android: androidDevices.length > 0
325470
+ ios: iosDevices.some(isReadyDevice),
325471
+ android: androidDevices.some(isReadyDevice)
325463
325472
  };
325464
325473
  }
325465
325474
 
@@ -325861,7 +325870,7 @@ async function executeMobileTest(options) {
325861
325870
  const devices = discoverAllDevices({ includeApps: true });
325862
325871
  const platformFilter = options.target.platform;
325863
325872
  const matchingDevices = devices.filter(
325864
- (d) => d.platform === platformFilter && d.state === "BOOTED"
325873
+ (d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
325865
325874
  );
325866
325875
  if (matchingDevices.length === 0) {
325867
325876
  throw new Error(
@@ -326025,7 +326034,7 @@ async function createMobileDiscoverySession(target) {
326025
326034
  const devices = discoverAllDevices({ includeApps: true });
326026
326035
  const platformFilter = target.platform;
326027
326036
  const matchingDevices = devices.filter(
326028
- (d) => d.platform === platformFilter && d.state === "BOOTED"
326037
+ (d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
326029
326038
  );
326030
326039
  if (matchingDevices.length === 0) {
326031
326040
  throw new Error(
@@ -326076,6 +326085,9 @@ async function createMobileDiscoverySession(target) {
326076
326085
  var import_http = require("http");
326077
326086
  var import_child_process3 = require("child_process");
326078
326087
  var import_crypto = require("crypto");
326088
+ var import_os3 = require("os");
326089
+ var import_path2 = require("path");
326090
+ var import_fs2 = require("fs");
326079
326091
  init_dist();
326080
326092
 
326081
326093
  // src/services/mobile/appium-actions.ts
@@ -326306,6 +326318,7 @@ var polling = false;
326306
326318
  var sessionStartInFlight = null;
326307
326319
  var sessionReaperTimer = null;
326308
326320
  var mirrorClientsEmptyAt = null;
326321
+ var mirrorSessionOwner = null;
326309
326322
  var executorBusy = false;
326310
326323
  var rateLimitBuckets = /* @__PURE__ */ new Map();
326311
326324
  function isRateLimited(clientIp) {
@@ -326361,30 +326374,50 @@ async function getWindowSize3() {
326361
326374
  return getWindowSize2(`/session/${state.appiumSessionId}`);
326362
326375
  }
326363
326376
  var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
326364
- function killPort2(port) {
326365
- if (!Number.isInteger(port) || port <= 0 || port > 65535) return;
326377
+ var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
326378
+ var BRIDGE_PID_FILE = (0, import_path2.join)((0, import_os3.homedir)(), ".validate.qa", "mobile-bridge.pid");
326379
+ function readBridgePidFile() {
326366
326380
  try {
326367
- const pids = (0, import_child_process3.execFileSync)("lsof", ["-ti", `:${port}`], { encoding: "utf-8" }).trim();
326368
- if (pids) {
326369
- const pidList = pids.split("\n").filter(Boolean);
326370
- for (const pid of pidList) {
326371
- if (!/^\d+$/.test(pid)) continue;
326372
- let command = "";
326373
- try {
326374
- command = (0, import_child_process3.execFileSync)("ps", ["-p", pid, "-o", "command="], { encoding: "utf-8" }).trim();
326375
- } catch {
326376
- continue;
326377
- }
326378
- if (!command.includes(BRIDGE_PROCESS_MARKER)) {
326379
- continue;
326380
- }
326381
- try {
326382
- (0, import_child_process3.execFileSync)("kill", ["-9", pid], { stdio: "ignore" });
326383
- } catch {
326384
- }
326385
- }
326381
+ const raw = (0, import_fs2.readFileSync)(BRIDGE_PID_FILE, "utf-8").trim();
326382
+ const pid = parseInt(raw, 10);
326383
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
326384
+ } catch {
326385
+ return null;
326386
+ }
326387
+ }
326388
+ function writeBridgePidFile(pid) {
326389
+ try {
326390
+ (0, import_fs2.mkdirSync)((0, import_path2.dirname)(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
326391
+ (0, import_fs2.writeFileSync)(BRIDGE_PID_FILE, String(pid), { mode: 384 });
326392
+ } catch {
326393
+ }
326394
+ }
326395
+ function clearBridgePidFile() {
326396
+ try {
326397
+ (0, import_fs2.unlinkSync)(BRIDGE_PID_FILE);
326398
+ } catch {
326399
+ }
326400
+ }
326401
+ function killStaleBridge() {
326402
+ const pid = readBridgePidFile();
326403
+ if (!pid) return;
326404
+ try {
326405
+ (0, import_child_process3.execFileSync)("kill", ["-0", String(pid)], { stdio: "ignore" });
326406
+ } catch {
326407
+ clearBridgePidFile();
326408
+ return;
326409
+ }
326410
+ try {
326411
+ const pidsOnPort = (0, import_child_process3.execFileSync)("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
326412
+ const pidList = pidsOnPort.split("\n").filter(Boolean);
326413
+ if (!pidList.includes(String(pid))) {
326414
+ return;
326386
326415
  }
326416
+ (0, import_child_process3.execFileSync)("kill", ["-9", String(pid)], { stdio: "ignore" });
326417
+ (0, import_child_process3.execFileSync)("sleep", ["1"], { stdio: "ignore" });
326387
326418
  } catch {
326419
+ } finally {
326420
+ clearBridgePidFile();
326388
326421
  }
326389
326422
  }
326390
326423
  function parseBody(req) {
@@ -326622,6 +326655,7 @@ async function startSession(validated, res) {
326622
326655
  }
326623
326656
  state.appiumSessionId = null;
326624
326657
  state.platform = null;
326658
+ mirrorSessionOwner = null;
326625
326659
  }
326626
326660
  stopAllMirrorClients();
326627
326661
  const realCaps = isPhysicalDevice(deviceId, targetPlatform) ? realDeviceCapabilities2(targetPlatform) : {};
@@ -326652,11 +326686,14 @@ async function startSession(validated, res) {
326652
326686
  })
326653
326687
  });
326654
326688
  state.appiumSessionId = result?.value?.sessionId ?? result?.sessionId ?? null;
326655
- state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
326656
326689
  if (!state.appiumSessionId) {
326690
+ state.platform = null;
326691
+ mirrorSessionOwner = null;
326657
326692
  sendJSON(res, 500, { error: "Appium session creation returned no session ID" });
326658
326693
  return;
326659
326694
  }
326695
+ state.platform = targetPlatform === "IOS" ? "IOS" : "ANDROID";
326696
+ mirrorSessionOwner = "recording";
326660
326697
  if (launchMode === "DEEP_LINK" && deeplink) {
326661
326698
  try {
326662
326699
  const args = targetPlatform === "IOS" ? [{ url: deeplink, bundleId: appId }] : [{ url: deeplink, package: appId }];
@@ -326681,6 +326718,7 @@ async function handleSessionStop(_req, res) {
326681
326718
  }
326682
326719
  state.appiumSessionId = null;
326683
326720
  state.platform = null;
326721
+ mirrorSessionOwner = null;
326684
326722
  }
326685
326723
  stopAllMirrorClients();
326686
326724
  stopSessionReaper();
@@ -326761,6 +326799,7 @@ async function pollAndBroadcastFrame() {
326761
326799
  if (!alive) {
326762
326800
  state.appiumSessionId = null;
326763
326801
  state.platform = null;
326802
+ mirrorSessionOwner = null;
326764
326803
  stopAllMirrorClients();
326765
326804
  return;
326766
326805
  }
@@ -326831,6 +326870,7 @@ async function reapIdleSession() {
326831
326870
  const sessionId = state.appiumSessionId;
326832
326871
  state.appiumSessionId = null;
326833
326872
  state.platform = null;
326873
+ mirrorSessionOwner = null;
326834
326874
  mirrorClientsEmptyAt = null;
326835
326875
  stopSessionReaper();
326836
326876
  try {
@@ -327009,7 +327049,7 @@ async function startMobileBridge(options) {
327009
327049
  if (!process.title.includes(BRIDGE_PROCESS_MARKER)) {
327010
327050
  process.title = `${process.title} ${BRIDGE_PROCESS_MARKER}`;
327011
327051
  }
327012
- killPort2(port);
327052
+ killStaleBridge();
327013
327053
  return new Promise((resolve, reject) => {
327014
327054
  server2 = (0, import_http.createServer)(route);
327015
327055
  server2.on("error", (err) => {
@@ -327025,6 +327065,7 @@ async function startMobileBridge(options) {
327025
327065
  state.port = port;
327026
327066
  state.tunnelToken = tunnelToken;
327027
327067
  state.tunnelUrl = void 0;
327068
+ writeBridgePidFile(process.pid);
327028
327069
  let tunnelUrl;
327029
327070
  if (enableTunnel) {
327030
327071
  try {
@@ -327092,6 +327133,7 @@ async function startMobileBridge(options) {
327092
327133
  let checkInterval = null;
327093
327134
  const timeout = setTimeout(() => {
327094
327135
  if (checkInterval) clearInterval(checkInterval);
327136
+ checkInterval = null;
327095
327137
  if (!tunnelUrl) {
327096
327138
  reject2(new Error("ngrok tunnel failed to start"));
327097
327139
  }
@@ -327100,6 +327142,7 @@ async function startMobileBridge(options) {
327100
327142
  if (tunnelUrl) {
327101
327143
  clearTimeout(timeout);
327102
327144
  if (checkInterval) clearInterval(checkInterval);
327145
+ checkInterval = null;
327103
327146
  resolve2();
327104
327147
  }
327105
327148
  }, 500);
@@ -327129,6 +327172,7 @@ async function startMobileBridge(options) {
327129
327172
  async function stopMobileBridge() {
327130
327173
  stopAllMirrorClients();
327131
327174
  stopSessionReaper();
327175
+ clearBridgePidFile();
327132
327176
  if (state.appiumSessionId) {
327133
327177
  try {
327134
327178
  validateSessionId(state.appiumSessionId);
@@ -327137,6 +327181,7 @@ async function stopMobileBridge() {
327137
327181
  }
327138
327182
  state.appiumSessionId = null;
327139
327183
  state.platform = null;
327184
+ mirrorSessionOwner = null;
327140
327185
  }
327141
327186
  if (ngrokProcess) {
327142
327187
  try {
@@ -327167,13 +327212,37 @@ function getBridgeState() {
327167
327212
  tunnelUrl: state.tunnelUrl,
327168
327213
  tunnelToken: state.tunnelToken,
327169
327214
  hasActiveSession: !!state.appiumSessionId,
327170
- recordingActive: !!state.appiumSessionId,
327215
+ recordingActive: !!state.appiumSessionId && mirrorSessionOwner === "recording",
327171
327216
  platform: state.platform
327172
327217
  };
327173
327218
  }
327174
327219
  function setExecutorBusy(busy) {
327175
327220
  executorBusy = busy;
327176
327221
  }
327222
+ function attachMirrorSession(sessionId, platform3) {
327223
+ validateSessionId(sessionId);
327224
+ if (state.appiumSessionId && state.appiumSessionId !== sessionId) {
327225
+ stopAllMirrorClients();
327226
+ }
327227
+ state.appiumSessionId = sessionId;
327228
+ state.platform = platform3;
327229
+ mirrorSessionOwner = "runner";
327230
+ mirrorNullTicks = 0;
327231
+ mirrorClientsEmptyAt = null;
327232
+ ensureSessionReaper();
327233
+ if (state.mirrorClients.size > 0) {
327234
+ ensureMirrorPolling();
327235
+ void pollAndBroadcastFrame();
327236
+ }
327237
+ }
327238
+ function detachMirrorSession(sessionId) {
327239
+ if (sessionId && state.appiumSessionId && state.appiumSessionId !== sessionId) return;
327240
+ state.appiumSessionId = null;
327241
+ state.platform = null;
327242
+ mirrorSessionOwner = null;
327243
+ stopAllMirrorClients();
327244
+ stopSessionReaper();
327245
+ }
327177
327246
 
327178
327247
  // src/services/mobile/mobile-bridge-driver.ts
327179
327248
  var import_runner_core7 = __toESM(require_dist2());
@@ -327989,6 +328058,219 @@ async function startMobileNetworkCapture(options) {
327989
328058
  };
327990
328059
  }
327991
328060
 
328061
+ // src/services/doctor.ts
328062
+ var import_child_process4 = require("child_process");
328063
+ var import_fs3 = require("fs");
328064
+ var import_os4 = require("os");
328065
+ var C = {
328066
+ reset: "\x1B[0m",
328067
+ bold: "\x1B[1m",
328068
+ green: "\x1B[32m",
328069
+ red: "\x1B[31m",
328070
+ yellow: "\x1B[33m",
328071
+ cyan: "\x1B[36m",
328072
+ gray: "\x1B[90m"
328073
+ };
328074
+ function run(cmd) {
328075
+ try {
328076
+ return (0, import_child_process4.execSync)(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
328077
+ } catch {
328078
+ return null;
328079
+ }
328080
+ }
328081
+ function check(name, fn) {
328082
+ try {
328083
+ const result = fn();
328084
+ return { name, ...result };
328085
+ } catch (err) {
328086
+ return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
328087
+ }
328088
+ }
328089
+ function checkWeb() {
328090
+ const results = [];
328091
+ results.push(check("Node.js", () => {
328092
+ const version = process.version;
328093
+ const major = parseInt(version.slice(1).split(".")[0], 10);
328094
+ return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
328095
+ }));
328096
+ results.push(check("Playwright", () => {
328097
+ const version = run("npx playwright --version");
328098
+ if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
328099
+ return { ok: true, detail: version };
328100
+ }));
328101
+ return results;
328102
+ }
328103
+ function checkIOS() {
328104
+ const results = [];
328105
+ const isMac = (0, import_os4.platform)() === "darwin";
328106
+ results.push(check("macOS", () => ({
328107
+ ok: isMac,
328108
+ detail: isMac ? `${(0, import_os4.platform)()} detected` : "iOS testing requires macOS"
328109
+ })));
328110
+ if (!isMac) return results;
328111
+ results.push(check("Xcode", () => {
328112
+ const version = run("xcodebuild -version");
328113
+ if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
328114
+ const firstLine = version.split("\n")[0];
328115
+ return { ok: true, detail: firstLine };
328116
+ }));
328117
+ results.push(check("Xcode Command Line Tools", () => {
328118
+ const path = run("xcode-select -p");
328119
+ return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
328120
+ }));
328121
+ results.push(check("Appium xcuitest driver", () => {
328122
+ const drivers = run("appium driver list --installed --json");
328123
+ if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
328124
+ try {
328125
+ const parsed = JSON.parse(drivers);
328126
+ const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
328127
+ return {
328128
+ ok: !!hasXcuitest,
328129
+ detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
328130
+ };
328131
+ } catch {
328132
+ const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
328133
+ return {
328134
+ ok: hasXcuitest,
328135
+ detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
328136
+ };
328137
+ }
328138
+ }));
328139
+ results.push(check("iOS Devices", () => {
328140
+ const json = run("xcrun simctl list devices booted --json");
328141
+ let bootedSimulators = 0;
328142
+ if (json) {
328143
+ try {
328144
+ const parsed = JSON.parse(json);
328145
+ const devices = Object.values(parsed.devices ?? {}).flat();
328146
+ bootedSimulators = devices.filter((d) => d.state === "Booted").length;
328147
+ } catch {
328148
+ }
328149
+ }
328150
+ const realDevices = discoverRealIOSDevices().length;
328151
+ const total = bootedSimulators + realDevices;
328152
+ if (total === 0) {
328153
+ return {
328154
+ ok: false,
328155
+ detail: "No devices found. Boot a simulator or connect a real device via USB."
328156
+ };
328157
+ }
328158
+ const details = [];
328159
+ if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
328160
+ if (realDevices > 0) details.push(`${realDevices} real device(s)`);
328161
+ return {
328162
+ ok: true,
328163
+ detail: `${total} device(s) available: ${details.join(", ")}`
328164
+ };
328165
+ }));
328166
+ return results;
328167
+ }
328168
+ function checkAndroid() {
328169
+ const results = [];
328170
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
328171
+ results.push(check("ANDROID_HOME", () => ({
328172
+ ok: !!androidHome && (0, import_fs3.existsSync)(androidHome),
328173
+ detail: androidHome ? (0, import_fs3.existsSync)(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
328174
+ })));
328175
+ if (!androidHome || !(0, import_fs3.existsSync)(androidHome)) return results;
328176
+ results.push(check("adb", () => {
328177
+ const version = run("adb version");
328178
+ if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
328179
+ const firstLine = version.split("\n")[0];
328180
+ return { ok: true, detail: firstLine };
328181
+ }));
328182
+ results.push(check("Appium uiautomator2 driver", () => {
328183
+ const drivers = run("appium driver list --installed --json");
328184
+ if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
328185
+ try {
328186
+ const parsed = JSON.parse(drivers);
328187
+ const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
328188
+ return {
328189
+ ok: !!hasUia2,
328190
+ detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
328191
+ };
328192
+ } catch {
328193
+ const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
328194
+ return {
328195
+ ok: hasUia2,
328196
+ detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
328197
+ };
328198
+ }
328199
+ }));
328200
+ results.push(check("Android Devices", () => {
328201
+ const output = run("adb devices");
328202
+ if (!output) return { ok: false, detail: "adb devices failed" };
328203
+ const lines = output.split("\n").filter((l) => l.includes(" device"));
328204
+ const emulators = lines.filter((l) => l.startsWith("emulator-"));
328205
+ const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
328206
+ const total = emulators.length + realDevices.length;
328207
+ if (total === 0) {
328208
+ return {
328209
+ ok: false,
328210
+ detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
328211
+ };
328212
+ }
328213
+ const details = [];
328214
+ if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
328215
+ if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
328216
+ return {
328217
+ ok: true,
328218
+ detail: `${total} device(s) available: ${details.join(", ")}`
328219
+ };
328220
+ }));
328221
+ return results;
328222
+ }
328223
+ function checkTunnel() {
328224
+ const version = run("ngrok version") || run("ngrok --version");
328225
+ if (!version) {
328226
+ return [{
328227
+ name: "ngrok",
328228
+ ok: false,
328229
+ detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
328230
+ }];
328231
+ }
328232
+ const match = version.match(/(\d+)\.\d+\.\d+/);
328233
+ const major = match ? parseInt(match[1], 10) : 0;
328234
+ return [{
328235
+ name: "ngrok",
328236
+ ok: major >= 3,
328237
+ detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
328238
+ }];
328239
+ }
328240
+ function runDoctor() {
328241
+ const web = checkWeb();
328242
+ const ios = checkIOS();
328243
+ const android = checkAndroid();
328244
+ const tunnel = checkTunnel();
328245
+ const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
328246
+ return { web, ios, android, tunnel, allPassed };
328247
+ }
328248
+ function printDoctorReport(report) {
328249
+ console.log("");
328250
+ console.log(`${C.bold}${C.cyan}\u{1F3E5} validate.qa Doctor${C.reset}`);
328251
+ console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
328252
+ const printSection = (title, checks) => {
328253
+ console.log("");
328254
+ console.log(` ${C.bold}${title}:${C.reset}`);
328255
+ for (const c of checks) {
328256
+ const icon = c.ok ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`;
328257
+ console.log(` ${icon} ${c.name}: ${c.detail}`);
328258
+ }
328259
+ };
328260
+ printSection("Web Testing", report.web);
328261
+ printSection("iOS Testing", report.ios);
328262
+ printSection("Android Testing", report.android);
328263
+ printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
328264
+ console.log("");
328265
+ if (report.allPassed) {
328266
+ console.log(` ${C.green}\u2705 All checks passed - ready for web + mobile testing${C.reset}`);
328267
+ } else {
328268
+ const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
328269
+ console.log(` ${C.yellow}\u26A0 ${failCount} check(s) need attention${C.reset}`);
328270
+ }
328271
+ console.log("");
328272
+ }
328273
+
327992
328274
  // src/runner.ts
327993
328275
  init_dist();
327994
328276
  var import_runner_core11 = __toESM(require_dist2());
@@ -328078,7 +328360,7 @@ var browserPool = new import_runner_core6.BrowserPool({
328078
328360
  maxContextsPerBrowser: envInt("MAX_CONTEXTS_PER_BROWSER", DEFAULT_MAX_CONTEXTS_PER_BROWSER),
328079
328361
  retireAfterContexts: 30
328080
328362
  });
328081
- var C = {
328363
+ var C2 = {
328082
328364
  reset: "\x1B[0m",
328083
328365
  bold: "\x1B[1m",
328084
328366
  green: "\x1B[32m",
@@ -328088,25 +328370,25 @@ var C = {
328088
328370
  gray: "\x1B[90m"
328089
328371
  };
328090
328372
  var log = {
328091
- info: (msg) => console.log(`${C.cyan}\u2139${C.reset} ${msg}`),
328092
- ok: (msg) => console.log(`${C.green}\u2713${C.reset} ${msg}`),
328093
- fail: (msg) => console.log(`${C.red}\u2717${C.reset} ${msg}`),
328094
- warn: (msg) => console.log(`${C.yellow}\u26A0${C.reset} ${msg}`),
328095
- dim: (msg) => console.log(`${C.gray}${msg}${C.reset}`)
328373
+ info: (msg) => console.log(`${C2.cyan}\u2139${C2.reset} ${msg}`),
328374
+ ok: (msg) => console.log(`${C2.green}\u2713${C2.reset} ${msg}`),
328375
+ fail: (msg) => console.log(`${C2.red}\u2717${C2.reset} ${msg}`),
328376
+ warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
328377
+ dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
328096
328378
  };
328097
328379
  var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
328098
328380
  function getRunsDir() {
328099
- const dir = process.env.RUNS_DIR || (0, import_path2.join)((0, import_os3.homedir)(), ".validate.qa", "runs");
328100
- if (!(0, import_fs2.existsSync)(dir)) (0, import_fs2.mkdirSync)(dir, { recursive: true });
328381
+ const dir = process.env.RUNS_DIR || (0, import_path3.join)((0, import_os5.homedir)(), ".validate.qa", "runs");
328382
+ if (!(0, import_fs4.existsSync)(dir)) (0, import_fs4.mkdirSync)(dir, { recursive: true });
328101
328383
  return dir;
328102
328384
  }
328103
328385
  function authStatePath(projectId) {
328104
- return (0, import_path2.join)(getRunsDir(), `auth-state-${projectId || "default"}.json`);
328386
+ return (0, import_path3.join)(getRunsDir(), `auth-state-${projectId || "default"}.json`);
328105
328387
  }
328106
328388
  function getValidAuthState(projectId) {
328107
328389
  const path = authStatePath(projectId);
328108
- if (!(0, import_fs2.existsSync)(path)) return void 0;
328109
- const age = Date.now() - (0, import_fs2.statSync)(path).mtimeMs;
328390
+ if (!(0, import_fs4.existsSync)(path)) return void 0;
328391
+ const age = Date.now() - (0, import_fs4.statSync)(path).mtimeMs;
328110
328392
  return age < AUTH_MAX_AGE_MS ? path : void 0;
328111
328393
  }
328112
328394
  function buildHeaders(authToken, projectId) {
@@ -329048,9 +329330,9 @@ ${finalVerifyResult.logs ?? ""}`;
329048
329330
  (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")
329049
329331
  );
329050
329332
  if (phaseLines.length > 0) {
329051
- console.log(`${C.gray}\u2500\u2500 Heal summary \u2500\u2500${C.reset}`);
329333
+ console.log(`${C2.gray}\u2500\u2500 Heal summary \u2500\u2500${C2.reset}`);
329052
329334
  for (const line of phaseLines) console.log(line);
329053
- console.log(`${C.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C.reset}`);
329335
+ console.log(`${C2.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C2.reset}`);
329054
329336
  }
329055
329337
  }
329056
329338
  log[result2.passed ? "ok" : "fail"](
@@ -329589,11 +329871,18 @@ ${finalVerifyResult.logs ?? ""}`;
329589
329871
  }
329590
329872
  try {
329591
329873
  if (isNativeMobile) {
329592
- const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : "ANDROID";
329593
329874
  const mobileTarget = ctx.target;
329594
329875
  if (!mobileTarget || !mobileTarget.appId) {
329595
329876
  throw new Error("Native mobile discovery requires a target with an appId (bundleId/appPackage).");
329596
329877
  }
329878
+ const targetPlatform = mobileTarget.platform;
329879
+ const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : mobileMedium === "ANDROID_NATIVE" ? "ANDROID" : targetPlatform === "IOS" || targetPlatform === "ANDROID" ? targetPlatform : null;
329880
+ if (!platform3) {
329881
+ throw new Error("Native mobile discovery requires medium IOS_NATIVE/ANDROID_NATIVE or target.platform IOS/ANDROID.");
329882
+ }
329883
+ if (mobileMedium === "IOS_NATIVE" && targetPlatform === "ANDROID" || mobileMedium === "ANDROID_NATIVE" && targetPlatform === "IOS") {
329884
+ throw new Error("Native mobile discovery medium does not match target.platform.");
329885
+ }
329597
329886
  if (getBridgeState().hasActiveSession) {
329598
329887
  throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
329599
329888
  }
@@ -329612,6 +329901,7 @@ ${finalVerifyResult.logs ?? ""}`;
329612
329901
  deeplink: mobileTarget.deeplink
329613
329902
  });
329614
329903
  mobileOnLog(`Appium session created: ${session.sessionId} on device ${session.deviceId}`);
329904
+ attachMirrorSession(session.sessionId, platform3);
329615
329905
  const driver = new MobileBridgeDriver({
329616
329906
  sessionId: session.sessionId,
329617
329907
  platform: platform3,
@@ -329652,6 +329942,7 @@ ${finalVerifyResult.logs ?? ""}`;
329652
329942
  }
329653
329943
  }
329654
329944
  if (session) {
329945
+ detachMirrorSession(session.sessionId);
329655
329946
  try {
329656
329947
  await session.stop();
329657
329948
  } catch {
@@ -330022,9 +330313,9 @@ ${finalVerifyResult.logs ?? ""}`;
330022
330313
  } else {
330023
330314
  log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
330024
330315
  if (result.logs) {
330025
- console.log(`${C.gray}\u2500\u2500 Runner logs \u2500\u2500${C.reset}`);
330316
+ console.log(`${C2.gray}\u2500\u2500 Runner logs \u2500\u2500${C2.reset}`);
330026
330317
  console.log(result.logs);
330027
- console.log(`${C.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C.reset}`);
330318
+ console.log(`${C2.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C2.reset}`);
330028
330319
  }
330029
330320
  }
330030
330321
  } catch (err) {
@@ -330046,20 +330337,24 @@ ${finalVerifyResult.logs ?? ""}`;
330046
330337
  }
330047
330338
  async function startRunner(opts) {
330048
330339
  const { serverUrl, authToken, projectId, pollIntervalMs = 2e3, runnerMode = "playwright-native", enableMobile = false, enableMobileTunnel = false } = opts;
330340
+ process.env.SERVER_URL = serverUrl;
330341
+ process.env.AUTH_TOKEN = authToken;
330342
+ process.env.RUNNER_TOKEN = authToken;
330343
+ if (projectId) process.env.PROJECT_ID = projectId;
330049
330344
  const headers = buildHeaders(authToken, projectId);
330050
330345
  console.log("");
330051
- console.log(`${C.bold}${C.cyan}\u{1F399}\uFE0F validate.qa Local Runner${C.reset}`);
330052
- console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
330346
+ console.log(`${C2.bold}${C2.cyan}\u{1F399}\uFE0F validate.qa Local Runner${C2.reset}`);
330347
+ console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
330053
330348
  log.info(`Server: ${serverUrl}`);
330054
330349
  log.info(`Project: ${projectId}`);
330055
330350
  log.info(`Mode: \u{1F3AD} Playwright Native${enableMobile ? " + \u{1F4F1} Mobile" : ""}${enableMobile && enableMobileTunnel ? " \u{1F310} Tunnel" : ""}`);
330056
330351
  log.info(`Interval: ${pollIntervalMs}ms`);
330057
330352
  if (process.env.DISCOVERY_PROMPT_V3 === "true") {
330058
- log.ok("Discovery: V3 forced (DISCOVERY_PROMPT_V3=true)");
330353
+ log.ok("Discovery prompt: V3 (per-turn rebuild) \u2014 DISCOVERY_PROMPT_V3=true");
330059
330354
  } else if (process.env.DISCOVERY_PROMPT_V3 === "false") {
330060
- log.ok("Discovery: V2.5 forced (DISCOVERY_PROMPT_V3=false)");
330355
+ log.ok("Discovery prompt: V2.5 (full transcript + control brief) \u2014 DISCOVERY_PROMPT_V3=false");
330061
330356
  } else {
330062
- log.ok("Discovery: auto mode (Grok -> V2.5, others -> V3)");
330357
+ log.ok("Discovery prompt: V3 (per-turn rebuild, default) \u2014 set DISCOVERY_PROMPT_V3=false for V2.5");
330063
330358
  }
330064
330359
  if ((0, import_runner_core5.hasUsableChromeProfile)()) {
330065
330360
  log.ok(`Browser: Chrome with user profile (cookies/logins persist)`);
@@ -330071,8 +330366,8 @@ async function startRunner(opts) {
330071
330366
  const devices = discoverAllDevices();
330072
330367
  const capabilities = {
330073
330368
  web: true,
330074
- ios: devices.some((d) => d.platform === "IOS"),
330075
- android: devices.some((d) => d.platform === "ANDROID")
330369
+ ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
330370
+ android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
330076
330371
  };
330077
330372
  log.info(`\u{1F4F1} Mobile: iOS ${capabilities.ios ? "\u2713" : "\u2717"} | Android ${capabilities.android ? "\u2713" : "\u2717"} | ${devices.length} device(s)`);
330078
330373
  try {
@@ -330133,6 +330428,33 @@ async function startRunner(opts) {
330133
330428
  const startedAt = Date.now();
330134
330429
  const MAX_BACKOFF_MS = 6e4;
330135
330430
  let consecutivePollErrors = 0;
330431
+ let cachedDoctor = null;
330432
+ const DOCTOR_TTL_MS = 5 * 60 * 1e3;
330433
+ let doctorRefreshInFlight = false;
330434
+ const refreshDoctorInBackground = () => {
330435
+ if (doctorRefreshInFlight) return;
330436
+ doctorRefreshInFlight = true;
330437
+ setImmediate(() => {
330438
+ try {
330439
+ const report = runDoctor();
330440
+ cachedDoctor = { report, at: Date.now() };
330441
+ } catch (err) {
330442
+ log.dim(` [doctor] refresh failed: ${err instanceof Error ? err.message : String(err)}`);
330443
+ } finally {
330444
+ doctorRefreshInFlight = false;
330445
+ }
330446
+ });
330447
+ };
330448
+ const getCachedDoctor = () => {
330449
+ if (!enableMobile) return void 0;
330450
+ if (!cachedDoctor) {
330451
+ refreshDoctorInBackground();
330452
+ return void 0;
330453
+ }
330454
+ const now = Date.now();
330455
+ if (now - cachedDoctor.at >= DOCTOR_TTL_MS) refreshDoctorInBackground();
330456
+ return cachedDoctor.report;
330457
+ };
330136
330458
  log.info(`Concurrency: ${RESOURCE_GOVERNOR_OPTIONS.adaptive ? `adaptive (${RESOURCE_GOVERNOR_OPTIONS.maxUnits} weighted units, hard cap ${MAX_CONCURRENT})` : `static (hard cap ${MAX_CONCURRENT})`}`);
330137
330459
  const poolSize = envInt("BROWSER_POOL_SIZE", DEFAULT_BROWSER_POOL_SIZE);
330138
330460
  if (poolSize > 0) {
@@ -330241,7 +330563,11 @@ async function startRunner(opts) {
330241
330563
  const sendHeartbeat = async () => {
330242
330564
  try {
330243
330565
  const devices = enableMobile ? discoverAllDevices() : [];
330244
- 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 };
330566
+ const capabilities = enableMobile ? {
330567
+ web: true,
330568
+ ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
330569
+ android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
330570
+ } : { web: true, ios: false, android: false };
330245
330571
  const bridge = enableMobile ? getBridgeState() : void 0;
330246
330572
  const resourceStatus = (0, import_runner_core11.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
330247
330573
  const processInventory = await (0, import_runner_core11.getBrowserProcessInventory)();
@@ -330272,7 +330598,10 @@ async function startRunner(opts) {
330272
330598
  resourceGovernor: resourceStatus.governor,
330273
330599
  processInventory,
330274
330600
  aiQueue: (0, import_runner_core11.getRunnerAIQueueStats)(),
330275
- liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat()
330601
+ liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat(),
330602
+ // Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
330603
+ // Cached & cheap — see getCachedDoctor above.
330604
+ doctor: getCachedDoctor()
330276
330605
  }),
330277
330606
  signal: AbortSignal.timeout(1e4)
330278
330607
  });
@@ -330472,219 +330801,6 @@ async function startRunner(opts) {
330472
330801
  }
330473
330802
  }
330474
330803
 
330475
- // src/services/doctor.ts
330476
- var import_child_process4 = require("child_process");
330477
- var import_fs3 = require("fs");
330478
- var import_os4 = require("os");
330479
- var C2 = {
330480
- reset: "\x1B[0m",
330481
- bold: "\x1B[1m",
330482
- green: "\x1B[32m",
330483
- red: "\x1B[31m",
330484
- yellow: "\x1B[33m",
330485
- cyan: "\x1B[36m",
330486
- gray: "\x1B[90m"
330487
- };
330488
- function run(cmd) {
330489
- try {
330490
- return (0, import_child_process4.execSync)(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
330491
- } catch {
330492
- return null;
330493
- }
330494
- }
330495
- function check(name, fn) {
330496
- try {
330497
- const result = fn();
330498
- return { name, ...result };
330499
- } catch (err) {
330500
- return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
330501
- }
330502
- }
330503
- function checkWeb() {
330504
- const results = [];
330505
- results.push(check("Node.js", () => {
330506
- const version = process.version;
330507
- const major = parseInt(version.slice(1).split(".")[0], 10);
330508
- return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
330509
- }));
330510
- results.push(check("Playwright", () => {
330511
- const version = run("npx playwright --version");
330512
- if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
330513
- return { ok: true, detail: version };
330514
- }));
330515
- return results;
330516
- }
330517
- function checkIOS() {
330518
- const results = [];
330519
- const isMac = (0, import_os4.platform)() === "darwin";
330520
- results.push(check("macOS", () => ({
330521
- ok: isMac,
330522
- detail: isMac ? `${(0, import_os4.platform)()} detected` : "iOS testing requires macOS"
330523
- })));
330524
- if (!isMac) return results;
330525
- results.push(check("Xcode", () => {
330526
- const version = run("xcodebuild -version");
330527
- if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
330528
- const firstLine = version.split("\n")[0];
330529
- return { ok: true, detail: firstLine };
330530
- }));
330531
- results.push(check("Xcode Command Line Tools", () => {
330532
- const path = run("xcode-select -p");
330533
- return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
330534
- }));
330535
- results.push(check("Appium xcuitest driver", () => {
330536
- const drivers = run("appium driver list --installed --json");
330537
- if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
330538
- try {
330539
- const parsed = JSON.parse(drivers);
330540
- const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
330541
- return {
330542
- ok: !!hasXcuitest,
330543
- detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
330544
- };
330545
- } catch {
330546
- const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
330547
- return {
330548
- ok: hasXcuitest,
330549
- detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
330550
- };
330551
- }
330552
- }));
330553
- results.push(check("iOS Devices", () => {
330554
- const json = run("xcrun simctl list devices booted --json");
330555
- let bootedSimulators = 0;
330556
- if (json) {
330557
- try {
330558
- const parsed = JSON.parse(json);
330559
- const devices = Object.values(parsed.devices ?? {}).flat();
330560
- bootedSimulators = devices.filter((d) => d.state === "Booted").length;
330561
- } catch {
330562
- }
330563
- }
330564
- const realDevices = discoverRealIOSDevices().length;
330565
- const total = bootedSimulators + realDevices;
330566
- if (total === 0) {
330567
- return {
330568
- ok: false,
330569
- detail: "No devices found. Boot a simulator or connect a real device via USB."
330570
- };
330571
- }
330572
- const details = [];
330573
- if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
330574
- if (realDevices > 0) details.push(`${realDevices} real device(s)`);
330575
- return {
330576
- ok: true,
330577
- detail: `${total} device(s) available: ${details.join(", ")}`
330578
- };
330579
- }));
330580
- return results;
330581
- }
330582
- function checkAndroid() {
330583
- const results = [];
330584
- const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
330585
- results.push(check("ANDROID_HOME", () => ({
330586
- ok: !!androidHome && (0, import_fs3.existsSync)(androidHome),
330587
- detail: androidHome ? (0, import_fs3.existsSync)(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
330588
- })));
330589
- if (!androidHome || !(0, import_fs3.existsSync)(androidHome)) return results;
330590
- results.push(check("adb", () => {
330591
- const version = run("adb version");
330592
- if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
330593
- const firstLine = version.split("\n")[0];
330594
- return { ok: true, detail: firstLine };
330595
- }));
330596
- results.push(check("Appium uiautomator2 driver", () => {
330597
- const drivers = run("appium driver list --installed --json");
330598
- if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
330599
- try {
330600
- const parsed = JSON.parse(drivers);
330601
- const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
330602
- return {
330603
- ok: !!hasUia2,
330604
- detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
330605
- };
330606
- } catch {
330607
- const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
330608
- return {
330609
- ok: hasUia2,
330610
- detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
330611
- };
330612
- }
330613
- }));
330614
- results.push(check("Android Devices", () => {
330615
- const output = run("adb devices");
330616
- if (!output) return { ok: false, detail: "adb devices failed" };
330617
- const lines = output.split("\n").filter((l) => l.includes(" device"));
330618
- const emulators = lines.filter((l) => l.startsWith("emulator-"));
330619
- const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
330620
- const total = emulators.length + realDevices.length;
330621
- if (total === 0) {
330622
- return {
330623
- ok: false,
330624
- detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
330625
- };
330626
- }
330627
- const details = [];
330628
- if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
330629
- if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
330630
- return {
330631
- ok: true,
330632
- detail: `${total} device(s) available: ${details.join(", ")}`
330633
- };
330634
- }));
330635
- return results;
330636
- }
330637
- function checkTunnel() {
330638
- const version = run("ngrok version") || run("ngrok --version");
330639
- if (!version) {
330640
- return [{
330641
- name: "ngrok",
330642
- ok: false,
330643
- detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
330644
- }];
330645
- }
330646
- const match = version.match(/(\d+)\.\d+\.\d+/);
330647
- const major = match ? parseInt(match[1], 10) : 0;
330648
- return [{
330649
- name: "ngrok",
330650
- ok: major >= 3,
330651
- detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
330652
- }];
330653
- }
330654
- function runDoctor() {
330655
- const web = checkWeb();
330656
- const ios = checkIOS();
330657
- const android = checkAndroid();
330658
- const tunnel = checkTunnel();
330659
- const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
330660
- return { web, ios, android, tunnel, allPassed };
330661
- }
330662
- function printDoctorReport(report) {
330663
- console.log("");
330664
- console.log(`${C2.bold}${C2.cyan}\u{1F3E5} validate.qa Doctor${C2.reset}`);
330665
- console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
330666
- const printSection = (title, checks) => {
330667
- console.log("");
330668
- console.log(` ${C2.bold}${title}:${C2.reset}`);
330669
- for (const c of checks) {
330670
- const icon = c.ok ? `${C2.green}\u2713${C2.reset}` : `${C2.red}\u2717${C2.reset}`;
330671
- console.log(` ${icon} ${c.name}: ${c.detail}`);
330672
- }
330673
- };
330674
- printSection("Web Testing", report.web);
330675
- printSection("iOS Testing", report.ios);
330676
- printSection("Android Testing", report.android);
330677
- printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
330678
- console.log("");
330679
- if (report.allPassed) {
330680
- console.log(` ${C2.green}\u2705 All checks passed - ready for web + mobile testing${C2.reset}`);
330681
- } else {
330682
- const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
330683
- console.log(` ${C2.yellow}\u26A0 ${failCount} check(s) need attention${C2.reset}`);
330684
- }
330685
- console.log("");
330686
- }
330687
-
330688
330804
  // src/cli.ts
330689
330805
  var pkg = require_package4();
330690
330806
  var program = new import_commander.Command();