@validate.qa/runner 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +407 -330
  2. package/dist/cli.mjs +412 -335
  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) {
@@ -21743,7 +21743,7 @@ var require_util = __commonJS({
21743
21743
  return path;
21744
21744
  }
21745
21745
  exports.normalize = normalize;
21746
- function join4(aRoot, aPath) {
21746
+ function join5(aRoot, aPath) {
21747
21747
  if (aRoot === "") {
21748
21748
  aRoot = ".";
21749
21749
  }
@@ -21775,7 +21775,7 @@ var require_util = __commonJS({
21775
21775
  }
21776
21776
  return joined;
21777
21777
  }
21778
- exports.join = join4;
21778
+ exports.join = join5;
21779
21779
  exports.isAbsolute = function(aPath) {
21780
21780
  return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
21781
21781
  };
@@ -21948,7 +21948,7 @@ var require_util = __commonJS({
21948
21948
  parsed.path = parsed.path.substring(0, index + 1);
21949
21949
  }
21950
21950
  }
21951
- sourceURL = join4(urlGenerate(parsed), sourceURL);
21951
+ sourceURL = join5(urlGenerate(parsed), sourceURL);
21952
21952
  }
21953
21953
  return normalize(sourceURL);
21954
21954
  }
@@ -243103,7 +243103,7 @@ var require_thread_stream = __commonJS({
243103
243103
  var { version } = require_package();
243104
243104
  var { EventEmitter: EventEmitter2 } = __require("events");
243105
243105
  var { Worker } = __require("worker_threads");
243106
- var { join: join4 } = __require("path");
243106
+ var { join: join5 } = __require("path");
243107
243107
  var { pathToFileURL } = __require("url");
243108
243108
  var { wait } = require_wait();
243109
243109
  var {
@@ -243154,7 +243154,7 @@ var require_thread_stream = __commonJS({
243154
243154
  function createWorker(stream, opts) {
243155
243155
  const { filename, workerData } = opts;
243156
243156
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
243157
- const toExecute = bundlerOverrides["thread-stream-worker"] || join4(__dirname, "lib", "worker.js");
243157
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join5(__dirname, "lib", "worker.js");
243158
243158
  const worker = new Worker(toExecute, {
243159
243159
  ...opts.workerOpts,
243160
243160
  name: opts.workerOpts?.name || "thread-stream",
@@ -243622,7 +243622,7 @@ var require_transport = __commonJS({
243622
243622
  var { createRequire } = __require("module");
243623
243623
  var { existsSync: existsSync4 } = __require("fs");
243624
243624
  var getCallers = require_caller();
243625
- var { join: join4, isAbsolute, sep } = __require("path");
243625
+ var { join: join5, isAbsolute, sep } = __require("path");
243626
243626
  var { fileURLToPath } = __require("url");
243627
243627
  var sleep = require_atomic_sleep();
243628
243628
  var onExit = require_on_exit_leak_free();
@@ -243775,7 +243775,7 @@ var require_transport = __commonJS({
243775
243775
  throw new Error("only one of target or targets can be specified");
243776
243776
  }
243777
243777
  if (targets) {
243778
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
243778
+ target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
243779
243779
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
243780
243780
  return {
243781
243781
  ...dest,
@@ -243793,7 +243793,7 @@ var require_transport = __commonJS({
243793
243793
  });
243794
243794
  });
243795
243795
  } else if (pipeline) {
243796
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
243796
+ target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
243797
243797
  options.pipelines = [pipeline.map((dest) => {
243798
243798
  return {
243799
243799
  ...dest,
@@ -243816,7 +243816,7 @@ var require_transport = __commonJS({
243816
243816
  return origin;
243817
243817
  }
243818
243818
  if (origin === "pino/file") {
243819
- return join4(__dirname, "..", "file.js");
243819
+ return join5(__dirname, "..", "file.js");
243820
243820
  }
243821
243821
  let fixTarget2;
243822
243822
  for (const filePath of callers) {
@@ -244797,7 +244797,7 @@ var require_safe_stable_stringify = __commonJS({
244797
244797
  return circularValue;
244798
244798
  }
244799
244799
  let res = "";
244800
- let join4 = ",";
244800
+ let join5 = ",";
244801
244801
  const originalIndentation = indentation;
244802
244802
  if (Array.isArray(value)) {
244803
244803
  if (value.length === 0) {
@@ -244811,7 +244811,7 @@ var require_safe_stable_stringify = __commonJS({
244811
244811
  indentation += spacer;
244812
244812
  res += `
244813
244813
  ${indentation}`;
244814
- join4 = `,
244814
+ join5 = `,
244815
244815
  ${indentation}`;
244816
244816
  }
244817
244817
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -244819,13 +244819,13 @@ ${indentation}`;
244819
244819
  for (; i < maximumValuesToStringify - 1; i++) {
244820
244820
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
244821
244821
  res += tmp2 !== void 0 ? tmp2 : "null";
244822
- res += join4;
244822
+ res += join5;
244823
244823
  }
244824
244824
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
244825
244825
  res += tmp !== void 0 ? tmp : "null";
244826
244826
  if (value.length - 1 > maximumBreadth) {
244827
244827
  const removedKeys = value.length - maximumBreadth - 1;
244828
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
244828
+ res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
244829
244829
  }
244830
244830
  if (spacer !== "") {
244831
244831
  res += `
@@ -244846,7 +244846,7 @@ ${originalIndentation}`;
244846
244846
  let separator = "";
244847
244847
  if (spacer !== "") {
244848
244848
  indentation += spacer;
244849
- join4 = `,
244849
+ join5 = `,
244850
244850
  ${indentation}`;
244851
244851
  whitespace = " ";
244852
244852
  }
@@ -244860,13 +244860,13 @@ ${indentation}`;
244860
244860
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
244861
244861
  if (tmp !== void 0) {
244862
244862
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
244863
- separator = join4;
244863
+ separator = join5;
244864
244864
  }
244865
244865
  }
244866
244866
  if (keyLength > maximumBreadth) {
244867
244867
  const removedKeys = keyLength - maximumBreadth;
244868
244868
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
244869
- separator = join4;
244869
+ separator = join5;
244870
244870
  }
244871
244871
  if (spacer !== "" && separator.length > 1) {
244872
244872
  res = `
@@ -244907,7 +244907,7 @@ ${originalIndentation}`;
244907
244907
  }
244908
244908
  const originalIndentation = indentation;
244909
244909
  let res = "";
244910
- let join4 = ",";
244910
+ let join5 = ",";
244911
244911
  if (Array.isArray(value)) {
244912
244912
  if (value.length === 0) {
244913
244913
  return "[]";
@@ -244920,7 +244920,7 @@ ${originalIndentation}`;
244920
244920
  indentation += spacer;
244921
244921
  res += `
244922
244922
  ${indentation}`;
244923
- join4 = `,
244923
+ join5 = `,
244924
244924
  ${indentation}`;
244925
244925
  }
244926
244926
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -244928,13 +244928,13 @@ ${indentation}`;
244928
244928
  for (; i < maximumValuesToStringify - 1; i++) {
244929
244929
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
244930
244930
  res += tmp2 !== void 0 ? tmp2 : "null";
244931
- res += join4;
244931
+ res += join5;
244932
244932
  }
244933
244933
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
244934
244934
  res += tmp !== void 0 ? tmp : "null";
244935
244935
  if (value.length - 1 > maximumBreadth) {
244936
244936
  const removedKeys = value.length - maximumBreadth - 1;
244937
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
244937
+ res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
244938
244938
  }
244939
244939
  if (spacer !== "") {
244940
244940
  res += `
@@ -244947,7 +244947,7 @@ ${originalIndentation}`;
244947
244947
  let whitespace = "";
244948
244948
  if (spacer !== "") {
244949
244949
  indentation += spacer;
244950
- join4 = `,
244950
+ join5 = `,
244951
244951
  ${indentation}`;
244952
244952
  whitespace = " ";
244953
244953
  }
@@ -244956,7 +244956,7 @@ ${indentation}`;
244956
244956
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
244957
244957
  if (tmp !== void 0) {
244958
244958
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
244959
- separator = join4;
244959
+ separator = join5;
244960
244960
  }
244961
244961
  }
244962
244962
  if (spacer !== "" && separator.length > 1) {
@@ -245014,20 +245014,20 @@ ${originalIndentation}`;
245014
245014
  indentation += spacer;
245015
245015
  let res2 = `
245016
245016
  ${indentation}`;
245017
- const join5 = `,
245017
+ const join6 = `,
245018
245018
  ${indentation}`;
245019
245019
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
245020
245020
  let i = 0;
245021
245021
  for (; i < maximumValuesToStringify - 1; i++) {
245022
245022
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
245023
245023
  res2 += tmp2 !== void 0 ? tmp2 : "null";
245024
- res2 += join5;
245024
+ res2 += join6;
245025
245025
  }
245026
245026
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
245027
245027
  res2 += tmp !== void 0 ? tmp : "null";
245028
245028
  if (value.length - 1 > maximumBreadth) {
245029
245029
  const removedKeys = value.length - maximumBreadth - 1;
245030
- res2 += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
245030
+ res2 += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
245031
245031
  }
245032
245032
  res2 += `
245033
245033
  ${originalIndentation}`;
@@ -245043,16 +245043,16 @@ ${originalIndentation}`;
245043
245043
  return '"[Object]"';
245044
245044
  }
245045
245045
  indentation += spacer;
245046
- const join4 = `,
245046
+ const join5 = `,
245047
245047
  ${indentation}`;
245048
245048
  let res = "";
245049
245049
  let separator = "";
245050
245050
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
245051
245051
  if (isTypedArrayWithEntries(value)) {
245052
- res += stringifyTypedArray(value, join4, maximumBreadth);
245052
+ res += stringifyTypedArray(value, join5, maximumBreadth);
245053
245053
  keys = keys.slice(value.length);
245054
245054
  maximumPropertiesToStringify -= value.length;
245055
- separator = join4;
245055
+ separator = join5;
245056
245056
  }
245057
245057
  if (deterministic) {
245058
245058
  keys = sort(keys, comparator);
@@ -245063,13 +245063,13 @@ ${indentation}`;
245063
245063
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
245064
245064
  if (tmp !== void 0) {
245065
245065
  res += `${separator}${strEscape(key2)}: ${tmp}`;
245066
- separator = join4;
245066
+ separator = join5;
245067
245067
  }
245068
245068
  }
245069
245069
  if (keyLength > maximumBreadth) {
245070
245070
  const removedKeys = keyLength - maximumBreadth;
245071
245071
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
245072
- separator = join4;
245072
+ separator = join5;
245073
245073
  }
245074
245074
  if (separator !== "") {
245075
245075
  res = `
@@ -285426,11 +285426,11 @@ var require_core = __commonJS({
285426
285426
  function mapOuter(r, f) {
285427
285427
  return r.matched ? f(r) : r;
285428
285428
  }
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)));
285429
+ function ab(pa, pb, join5) {
285430
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join5(ma.value, vb, data, i, j)));
285431
285431
  }
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))));
285432
+ function abc(pa, pb, pc, join5) {
285433
+ 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
285434
  }
285435
285435
  function action(f) {
285436
285436
  return (data, i) => {
@@ -312624,7 +312624,7 @@ var require_utils2 = __commonJS({
312624
312624
  module.exports = Utils;
312625
312625
  Utils.prototype.makeDir = function(folder) {
312626
312626
  const self = this;
312627
- function mkdirSync3(fpath) {
312627
+ function mkdirSync4(fpath) {
312628
312628
  let resolvedPath = fpath.split(self.sep)[0];
312629
312629
  fpath.split(self.sep).forEach(function(name) {
312630
312630
  if (!name || name.substr(-1, 1) === ":") return;
@@ -312642,7 +312642,7 @@ var require_utils2 = __commonJS({
312642
312642
  if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
312643
312643
  });
312644
312644
  }
312645
- mkdirSync3(folder);
312645
+ mkdirSync4(folder);
312646
312646
  };
312647
312647
  Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
312648
312648
  const self = this;
@@ -314261,8 +314261,8 @@ var require_adm_zip = __commonJS({
314261
314261
  return null;
314262
314262
  }
314263
314263
  function fixPath(zipPath) {
314264
- const { join: join4, normalize, sep } = pth.posix;
314265
- return join4(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
314264
+ const { join: join5, normalize, sep } = pth.posix;
314265
+ return join5(pth.isAbsolute(zipPath) ? "/" : ".", normalize(sep + zipPath.split("\\").join(sep) + sep));
314266
314266
  }
314267
314267
  function filenameFilter(filterfn) {
314268
314268
  if (filterfn instanceof RegExp) {
@@ -319675,14 +319675,14 @@ var require_path_arg = __commonJS({
319675
319675
  var require_find_made = __commonJS({
319676
319676
  "../../node_modules/tar/node_modules/mkdirp/lib/find-made.js"(exports, module) {
319677
319677
  "use strict";
319678
- var { dirname } = __require("path");
319678
+ var { dirname: dirname2 } = __require("path");
319679
319679
  var findMade = (opts, parent, path = void 0) => {
319680
319680
  if (path === parent)
319681
319681
  return Promise.resolve();
319682
319682
  return opts.statAsync(parent).then(
319683
319683
  (st) => st.isDirectory() ? path : void 0,
319684
319684
  // will fail later
319685
- (er) => er.code === "ENOENT" ? findMade(opts, dirname(parent), parent) : void 0
319685
+ (er) => er.code === "ENOENT" ? findMade(opts, dirname2(parent), parent) : void 0
319686
319686
  );
319687
319687
  };
319688
319688
  var findMadeSync = (opts, parent, path = void 0) => {
@@ -319691,7 +319691,7 @@ var require_find_made = __commonJS({
319691
319691
  try {
319692
319692
  return opts.statSync(parent).isDirectory() ? path : void 0;
319693
319693
  } catch (er) {
319694
- return er.code === "ENOENT" ? findMadeSync(opts, dirname(parent), parent) : void 0;
319694
+ return er.code === "ENOENT" ? findMadeSync(opts, dirname2(parent), parent) : void 0;
319695
319695
  }
319696
319696
  };
319697
319697
  module.exports = { findMade, findMadeSync };
@@ -319702,10 +319702,10 @@ var require_find_made = __commonJS({
319702
319702
  var require_mkdirp_manual = __commonJS({
319703
319703
  "../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-manual.js"(exports, module) {
319704
319704
  "use strict";
319705
- var { dirname } = __require("path");
319705
+ var { dirname: dirname2 } = __require("path");
319706
319706
  var mkdirpManual = (path, opts, made) => {
319707
319707
  opts.recursive = false;
319708
- const parent = dirname(path);
319708
+ const parent = dirname2(path);
319709
319709
  if (parent === path) {
319710
319710
  return opts.mkdirAsync(path, opts).catch((er) => {
319711
319711
  if (er.code !== "EISDIR")
@@ -319728,7 +319728,7 @@ var require_mkdirp_manual = __commonJS({
319728
319728
  });
319729
319729
  };
319730
319730
  var mkdirpManualSync = (path, opts, made) => {
319731
- const parent = dirname(path);
319731
+ const parent = dirname2(path);
319732
319732
  opts.recursive = false;
319733
319733
  if (parent === path) {
319734
319734
  try {
@@ -319764,12 +319764,12 @@ var require_mkdirp_manual = __commonJS({
319764
319764
  var require_mkdirp_native = __commonJS({
319765
319765
  "../../node_modules/tar/node_modules/mkdirp/lib/mkdirp-native.js"(exports, module) {
319766
319766
  "use strict";
319767
- var { dirname } = __require("path");
319767
+ var { dirname: dirname2 } = __require("path");
319768
319768
  var { findMade, findMadeSync } = require_find_made();
319769
319769
  var { mkdirpManual, mkdirpManualSync } = require_mkdirp_manual();
319770
319770
  var mkdirpNative = (path, opts) => {
319771
319771
  opts.recursive = true;
319772
- const parent = dirname(path);
319772
+ const parent = dirname2(path);
319773
319773
  if (parent === path)
319774
319774
  return opts.mkdirAsync(path, opts);
319775
319775
  return findMade(opts, path).then((made) => opts.mkdirAsync(path, opts).then(() => made).catch((er) => {
@@ -319781,7 +319781,7 @@ var require_mkdirp_native = __commonJS({
319781
319781
  };
319782
319782
  var mkdirpNativeSync = (path, opts) => {
319783
319783
  opts.recursive = true;
319784
- const parent = dirname(path);
319784
+ const parent = dirname2(path);
319785
319785
  if (parent === path)
319786
319786
  return opts.mkdirSync(path, opts);
319787
319787
  const made = findMadeSync(opts, path);
@@ -320186,7 +320186,7 @@ var require_path_reservations = __commonJS({
320186
320186
  var assert = __require("assert");
320187
320187
  var normalize = require_normalize_unicode();
320188
320188
  var stripSlashes = require_strip_trailing_slashes();
320189
- var { join: join4 } = __require("path");
320189
+ var { join: join5 } = __require("path");
320190
320190
  var platform3 = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform;
320191
320191
  var isWindows = platform3 === "win32";
320192
320192
  module.exports = () => {
@@ -320195,7 +320195,7 @@ var require_path_reservations = __commonJS({
320195
320195
  const getDirs = (path) => {
320196
320196
  const dirs = path.split("/").slice(0, -1).reduce((set, path2) => {
320197
320197
  if (set.length) {
320198
- path2 = join4(set[set.length - 1], path2);
320198
+ path2 = join5(set[set.length - 1], path2);
320199
320199
  }
320200
320200
  set.push(path2 || "/");
320201
320201
  return set;
@@ -320263,7 +320263,7 @@ var require_path_reservations = __commonJS({
320263
320263
  };
320264
320264
  const reserve = (paths, fn) => {
320265
320265
  paths = isWindows ? ["win32 parallelization disabled"] : paths.map((p) => {
320266
- return stripSlashes(join4(normalize(p))).toLowerCase();
320266
+ return stripSlashes(join5(normalize(p))).toLowerCase();
320267
320267
  });
320268
320268
  const dirs = new Set(
320269
320269
  paths.map((path) => getDirs(path)).reduce((a, b) => a.concat(b))
@@ -324801,7 +324801,7 @@ var require_package4 = __commonJS({
324801
324801
  "package.json"(exports, module) {
324802
324802
  module.exports = {
324803
324803
  name: "@validate.qa/runner",
324804
- version: "1.0.2",
324804
+ version: "1.0.3",
324805
324805
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
324806
324806
  bin: {
324807
324807
  "validate-runner": "dist/cli.js"
@@ -324809,7 +324809,7 @@ var require_package4 = __commonJS({
324809
324809
  scripts: {
324810
324810
  build: "tsup",
324811
324811
  dev: "tsx src/cli.ts",
324812
- "test:mobile": "node --import tsx --test src/services/mobile/mobile-network-capture.smoke.test.ts",
324812
+ "test:mobile": "node --import tsx --test src/runner.mobile-ai-env.test.ts src/services/mobile/mobile-network-capture.smoke.test.ts",
324813
324813
  prepublishOnly: "npm run build"
324814
324814
  },
324815
324815
  files: [
@@ -324887,9 +324887,9 @@ function getConfigDir() {
324887
324887
  }
324888
324888
 
324889
324889
  // 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";
324890
+ import { existsSync as existsSync3, statSync, mkdirSync as mkdirSync3 } from "fs";
324891
+ import { join as join4 } from "path";
324892
+ import { homedir as homedir3 } from "os";
324893
324893
  import { createHash } from "crypto";
324894
324894
 
324895
324895
  // src/services/playwright-native.ts
@@ -325302,11 +325302,12 @@ function discoverRealIOSDevices() {
325302
325302
  for (const [index, node] of iosNodes.entries()) {
325303
325303
  const serial = typeof node.serial_num === "string" && node.serial_num.trim() ? node.serial_num.trim() : `usb-ios-device-${index + 1}`;
325304
325304
  const name = typeof node._name === "string" && node._name.trim() ? node._name.trim() : "iOS Device";
325305
+ const version = runCommand("ideviceinfo", ["-u", serial, "-k", "ProductVersion"]) || "unknown";
325305
325306
  devices.push({
325306
325307
  id: serial,
325307
325308
  name,
325308
325309
  platform: "IOS",
325309
- platformVersion: "unknown",
325310
+ platformVersion: version,
325310
325311
  state: "BOOTED",
325311
325312
  isAvailable: true,
325312
325313
  isPhysical: true
@@ -325381,7 +325382,7 @@ function parseAdbDevices() {
325381
325382
  const output = runCommand("adb", ["devices", "-l"]);
325382
325383
  if (!output) return [];
325383
325384
  const lines = output.split("\n").slice(1);
325384
- return lines.filter((line) => line.includes(" ") || line.includes("device ")).map((line) => {
325385
+ return lines.filter((line) => line.trim().length > 0).map((line) => {
325385
325386
  const parts = line.trim().split(/\s+/);
325386
325387
  const serial = parts[0];
325387
325388
  const type = parts[1] ?? "";
@@ -325397,26 +325398,25 @@ function parseAdbDevices() {
325397
325398
  device: props["device"],
325398
325399
  transportId: props["transport_id"]
325399
325400
  };
325400
- }).filter((d) => d.type === "device");
325401
+ }).filter((d) => d.type === "device" || d.type === "unauthorized");
325401
325402
  }
325402
325403
  function getAndroidProp(serial, prop) {
325403
325404
  return runCommand("adb", ["-s", serial, "shell", "getprop", prop]);
325404
325405
  }
325405
325406
  function discoverAndroidDevices() {
325406
325407
  const adbDevices = parseAdbDevices();
325407
- return adbDevices.filter((d) => {
325408
- return d.type === "device";
325409
- }).map((d) => {
325408
+ return adbDevices.filter((d) => d.type === "device" || d.type === "unauthorized").map((d) => {
325410
325409
  const isEmulator = d.serial.startsWith("emulator-");
325410
+ const isAvailable = d.type === "device";
325411
325411
  const name = d.model || d.device || (isEmulator ? "Android Emulator" : "Android Device");
325412
- const platformVersion = getAndroidProp(d.serial, "ro.build.version.release") || "unknown";
325412
+ const platformVersion = isAvailable ? getAndroidProp(d.serial, "ro.build.version.release") || "unknown" : "unknown";
325413
325413
  return {
325414
325414
  id: d.serial,
325415
325415
  name,
325416
325416
  platform: "ANDROID",
325417
325417
  platformVersion,
325418
325418
  state: "BOOTED",
325419
- isAvailable: true,
325419
+ isAvailable,
325420
325420
  // Mark if it's a real device vs emulator
325421
325421
  ...isEmulator ? {} : { isPhysical: true }
325422
325422
  };
@@ -325449,10 +325449,14 @@ function discoverAllDevices(options) {
325449
325449
  const androidDevices = discoverAndroidDevices();
325450
325450
  if (options?.includeApps) {
325451
325451
  for (const device of iosDevices) {
325452
- device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
325452
+ if (device.isAvailable) {
325453
+ device.installedApps = listIOSInstalledApps(device.id, !!device.isPhysical);
325454
+ }
325453
325455
  }
325454
325456
  for (const device of androidDevices) {
325455
- device.installedApps = listAndroidInstalledApps(device.id);
325457
+ if (device.isAvailable) {
325458
+ device.installedApps = listAndroidInstalledApps(device.id);
325459
+ }
325456
325460
  }
325457
325461
  }
325458
325462
  return [...iosDevices, ...androidDevices];
@@ -325460,11 +325464,12 @@ function discoverAllDevices(options) {
325460
325464
  function getRunnerCapabilities() {
325461
325465
  const iosDevices = discoverIOSDevices();
325462
325466
  const androidDevices = discoverAndroidDevices();
325467
+ const isReadyDevice = (device) => device.isAvailable && device.state === "BOOTED";
325463
325468
  return {
325464
325469
  web: true,
325465
325470
  // Always capable of web testing via Playwright
325466
- ios: iosDevices.length > 0,
325467
- android: androidDevices.length > 0
325471
+ ios: iosDevices.some(isReadyDevice),
325472
+ android: androidDevices.some(isReadyDevice)
325468
325473
  };
325469
325474
  }
325470
325475
 
@@ -325866,7 +325871,7 @@ async function executeMobileTest(options) {
325866
325871
  const devices = discoverAllDevices({ includeApps: true });
325867
325872
  const platformFilter = options.target.platform;
325868
325873
  const matchingDevices = devices.filter(
325869
- (d) => d.platform === platformFilter && d.state === "BOOTED"
325874
+ (d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
325870
325875
  );
325871
325876
  if (matchingDevices.length === 0) {
325872
325877
  throw new Error(
@@ -326030,7 +326035,7 @@ async function createMobileDiscoverySession(target) {
326030
326035
  const devices = discoverAllDevices({ includeApps: true });
326031
326036
  const platformFilter = target.platform;
326032
326037
  const matchingDevices = devices.filter(
326033
- (d) => d.platform === platformFilter && d.state === "BOOTED"
326038
+ (d) => d.platform === platformFilter && d.isAvailable && d.state === "BOOTED"
326034
326039
  );
326035
326040
  if (matchingDevices.length === 0) {
326036
326041
  throw new Error(
@@ -326081,6 +326086,9 @@ async function createMobileDiscoverySession(target) {
326081
326086
  import { createServer } from "http";
326082
326087
  import { execFileSync as execFileSync3 } from "child_process";
326083
326088
  import { randomBytes, timingSafeEqual } from "crypto";
326089
+ import { homedir as homedir2 } from "os";
326090
+ import { join as join2, dirname } from "path";
326091
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync2 } from "fs";
326084
326092
  init_dist();
326085
326093
 
326086
326094
  // src/services/mobile/appium-actions.ts
@@ -326366,30 +326374,50 @@ async function getWindowSize3() {
326366
326374
  return getWindowSize2(`/session/${state.appiumSessionId}`);
326367
326375
  }
326368
326376
  var BRIDGE_PROCESS_MARKER = "validateqa-mobile-bridge";
326369
- function killPort2(port) {
326370
- if (!Number.isInteger(port) || port <= 0 || port > 65535) return;
326377
+ var BRIDGE_PORT = MOBILE_BRIDGE_PORT2;
326378
+ var BRIDGE_PID_FILE = join2(homedir2(), ".validate.qa", "mobile-bridge.pid");
326379
+ function readBridgePidFile() {
326371
326380
  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
- }
326381
+ const raw = readFileSync2(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
+ mkdirSync2(dirname(BRIDGE_PID_FILE), { recursive: true, mode: 448 });
326391
+ writeFileSync2(BRIDGE_PID_FILE, String(pid), { mode: 384 });
326392
+ } catch {
326393
+ }
326394
+ }
326395
+ function clearBridgePidFile() {
326396
+ try {
326397
+ unlinkSync2(BRIDGE_PID_FILE);
326398
+ } catch {
326399
+ }
326400
+ }
326401
+ function killStaleBridge() {
326402
+ const pid = readBridgePidFile();
326403
+ if (!pid) return;
326404
+ try {
326405
+ execFileSync3("kill", ["-0", String(pid)], { stdio: "ignore" });
326406
+ } catch {
326407
+ clearBridgePidFile();
326408
+ return;
326409
+ }
326410
+ try {
326411
+ const pidsOnPort = execFileSync3("lsof", ["-ti", `:${BRIDGE_PORT}`], { encoding: "utf-8" }).trim();
326412
+ const pidList = pidsOnPort.split("\n").filter(Boolean);
326413
+ if (!pidList.includes(String(pid))) {
326414
+ return;
326391
326415
  }
326416
+ execFileSync3("kill", ["-9", String(pid)], { stdio: "ignore" });
326417
+ execFileSync3("sleep", ["1"], { stdio: "ignore" });
326392
326418
  } catch {
326419
+ } finally {
326420
+ clearBridgePidFile();
326393
326421
  }
326394
326422
  }
326395
326423
  function parseBody(req) {
@@ -327014,7 +327042,7 @@ async function startMobileBridge(options) {
327014
327042
  if (!process.title.includes(BRIDGE_PROCESS_MARKER)) {
327015
327043
  process.title = `${process.title} ${BRIDGE_PROCESS_MARKER}`;
327016
327044
  }
327017
- killPort2(port);
327045
+ killStaleBridge();
327018
327046
  return new Promise((resolve, reject) => {
327019
327047
  server2 = createServer(route);
327020
327048
  server2.on("error", (err) => {
@@ -327030,6 +327058,7 @@ async function startMobileBridge(options) {
327030
327058
  state.port = port;
327031
327059
  state.tunnelToken = tunnelToken;
327032
327060
  state.tunnelUrl = void 0;
327061
+ writeBridgePidFile(process.pid);
327033
327062
  let tunnelUrl;
327034
327063
  if (enableTunnel) {
327035
327064
  try {
@@ -327097,6 +327126,7 @@ async function startMobileBridge(options) {
327097
327126
  let checkInterval = null;
327098
327127
  const timeout = setTimeout(() => {
327099
327128
  if (checkInterval) clearInterval(checkInterval);
327129
+ checkInterval = null;
327100
327130
  if (!tunnelUrl) {
327101
327131
  reject2(new Error("ngrok tunnel failed to start"));
327102
327132
  }
@@ -327105,6 +327135,7 @@ async function startMobileBridge(options) {
327105
327135
  if (tunnelUrl) {
327106
327136
  clearTimeout(timeout);
327107
327137
  if (checkInterval) clearInterval(checkInterval);
327138
+ checkInterval = null;
327108
327139
  resolve2();
327109
327140
  }
327110
327141
  }, 500);
@@ -327134,6 +327165,7 @@ async function startMobileBridge(options) {
327134
327165
  async function stopMobileBridge() {
327135
327166
  stopAllMirrorClients();
327136
327167
  stopSessionReaper();
327168
+ clearBridgePidFile();
327137
327169
  if (state.appiumSessionId) {
327138
327170
  try {
327139
327171
  validateSessionId(state.appiumSessionId);
@@ -327392,7 +327424,7 @@ import { execFile, execFileSync as execFileSync4 } from "child_process";
327392
327424
  import { randomUUID } from "crypto";
327393
327425
  import { mkdtemp, rm, writeFile, readFile, readdir, mkdir, unlink } from "fs/promises";
327394
327426
  import { tmpdir, networkInterfaces } from "os";
327395
- import { join as join2 } from "path";
327427
+ import { join as join3 } from "path";
327396
327428
  import { promisify } from "util";
327397
327429
  import {
327398
327430
  getLocal,
@@ -327404,7 +327436,7 @@ var MAX_BODY_CAPTURE_BYTES = 64 * 1024;
327404
327436
  var DEVICE_CMD_TIMEOUT_MS = 15e3;
327405
327437
  var CA_KEY_BITS = 2048;
327406
327438
  var ANDROID_EMULATOR_HOST_LOOPBACK = "10.0.2.2";
327407
- var PENDING_CLEANUP_DIR = join2(tmpdir(), "validateqa-mobile-capture-pending");
327439
+ var PENDING_CLEANUP_DIR = join3(tmpdir(), "validateqa-mobile-capture-pending");
327408
327440
  function rawHeadersToPairs(rawHeaders) {
327409
327441
  return rawHeaders.map(([name, value]) => ({ name, value }));
327410
327442
  }
@@ -327688,7 +327720,7 @@ async function generateCaCert(dir, onLog) {
327688
327720
  // rely on stop() to remove the cert from the device promptly after the run
327689
327721
  // rather than on a short expiry.
327690
327722
  });
327691
- const caCertPath = join2(dir, "validateqa-mobile-ca.pem");
327723
+ const caCertPath = join3(dir, "validateqa-mobile-ca.pem");
327692
327724
  await writeFile(caCertPath, cert, "utf-8");
327693
327725
  return { caCertPath, caKeyPem: key, caCertPem: cert };
327694
327726
  } catch (err) {
@@ -327794,7 +327826,7 @@ function errMsg(err) {
327794
327826
  var activeCleanups = /* @__PURE__ */ new Map();
327795
327827
  var exitHandlersInstalled = false;
327796
327828
  function markerPath(sessionKey) {
327797
- return join2(PENDING_CLEANUP_DIR, `${sessionKey}.json`);
327829
+ return join3(PENDING_CLEANUP_DIR, `${sessionKey}.json`);
327798
327830
  }
327799
327831
  async function writePendingCleanup(sessionKey, marker) {
327800
327832
  try {
@@ -327836,7 +327868,7 @@ async function reconcilePendingCleanups(onLog) {
327836
327868
  }
327837
327869
  for (const file of files) {
327838
327870
  if (!file.endsWith(".json")) continue;
327839
- const full = join2(PENDING_CLEANUP_DIR, file);
327871
+ const full = join3(PENDING_CLEANUP_DIR, file);
327840
327872
  try {
327841
327873
  const raw = await readFile(full, "utf-8");
327842
327874
  const marker = JSON.parse(raw);
@@ -327881,7 +327913,7 @@ async function startMobileNetworkCapture(options) {
327881
327913
  let caKeyPem = null;
327882
327914
  let caCertPem = null;
327883
327915
  try {
327884
- tmpDir = await mkdtemp(join2(tmpdir(), "validateqa-mobile-ca-"));
327916
+ tmpDir = await mkdtemp(join3(tmpdir(), "validateqa-mobile-ca-"));
327885
327917
  const ca = await generateCaCert(tmpDir, onLog);
327886
327918
  if (ca) {
327887
327919
  caCertPath = ca.caCertPath;
@@ -327997,6 +328029,219 @@ async function startMobileNetworkCapture(options) {
327997
328029
  };
327998
328030
  }
327999
328031
 
328032
+ // src/services/doctor.ts
328033
+ import { execSync } from "child_process";
328034
+ import { existsSync as existsSync2 } from "fs";
328035
+ import { platform as platform2 } from "os";
328036
+ var C = {
328037
+ reset: "\x1B[0m",
328038
+ bold: "\x1B[1m",
328039
+ green: "\x1B[32m",
328040
+ red: "\x1B[31m",
328041
+ yellow: "\x1B[33m",
328042
+ cyan: "\x1B[36m",
328043
+ gray: "\x1B[90m"
328044
+ };
328045
+ function run(cmd) {
328046
+ try {
328047
+ return execSync(cmd, { encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
328048
+ } catch {
328049
+ return null;
328050
+ }
328051
+ }
328052
+ function check(name, fn) {
328053
+ try {
328054
+ const result = fn();
328055
+ return { name, ...result };
328056
+ } catch (err) {
328057
+ return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
328058
+ }
328059
+ }
328060
+ function checkWeb() {
328061
+ const results = [];
328062
+ results.push(check("Node.js", () => {
328063
+ const version = process.version;
328064
+ const major = parseInt(version.slice(1).split(".")[0], 10);
328065
+ return { ok: major >= 20, detail: `${version}${major < 20 ? " (>= 20 required)" : ""}` };
328066
+ }));
328067
+ results.push(check("Playwright", () => {
328068
+ const version = run("npx playwright --version");
328069
+ if (!version) return { ok: false, detail: "Not installed. Run: npm i -D playwright" };
328070
+ return { ok: true, detail: version };
328071
+ }));
328072
+ return results;
328073
+ }
328074
+ function checkIOS() {
328075
+ const results = [];
328076
+ const isMac = platform2() === "darwin";
328077
+ results.push(check("macOS", () => ({
328078
+ ok: isMac,
328079
+ detail: isMac ? `${platform2()} detected` : "iOS testing requires macOS"
328080
+ })));
328081
+ if (!isMac) return results;
328082
+ results.push(check("Xcode", () => {
328083
+ const version = run("xcodebuild -version");
328084
+ if (!version) return { ok: false, detail: "Not installed. Install from App Store." };
328085
+ const firstLine = version.split("\n")[0];
328086
+ return { ok: true, detail: firstLine };
328087
+ }));
328088
+ results.push(check("Xcode Command Line Tools", () => {
328089
+ const path = run("xcode-select -p");
328090
+ return { ok: !!path, detail: path || "Not installed. Run: xcode-select --install" };
328091
+ }));
328092
+ results.push(check("Appium xcuitest driver", () => {
328093
+ const drivers = run("appium driver list --installed --json");
328094
+ if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
328095
+ try {
328096
+ const parsed = JSON.parse(drivers);
328097
+ const hasXcuitest = parsed?.xcuitest || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("xcuitest"));
328098
+ return {
328099
+ ok: !!hasXcuitest,
328100
+ detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
328101
+ };
328102
+ } catch {
328103
+ const hasXcuitest = drivers.toLowerCase().includes("xcuitest");
328104
+ return {
328105
+ ok: hasXcuitest,
328106
+ detail: hasXcuitest ? "Installed" : "Not installed. Run: appium driver install xcuitest"
328107
+ };
328108
+ }
328109
+ }));
328110
+ results.push(check("iOS Devices", () => {
328111
+ const json = run("xcrun simctl list devices booted --json");
328112
+ let bootedSimulators = 0;
328113
+ if (json) {
328114
+ try {
328115
+ const parsed = JSON.parse(json);
328116
+ const devices = Object.values(parsed.devices ?? {}).flat();
328117
+ bootedSimulators = devices.filter((d) => d.state === "Booted").length;
328118
+ } catch {
328119
+ }
328120
+ }
328121
+ const realDevices = discoverRealIOSDevices().length;
328122
+ const total = bootedSimulators + realDevices;
328123
+ if (total === 0) {
328124
+ return {
328125
+ ok: false,
328126
+ detail: "No devices found. Boot a simulator or connect a real device via USB."
328127
+ };
328128
+ }
328129
+ const details = [];
328130
+ if (bootedSimulators > 0) details.push(`${bootedSimulators} simulator(s)`);
328131
+ if (realDevices > 0) details.push(`${realDevices} real device(s)`);
328132
+ return {
328133
+ ok: true,
328134
+ detail: `${total} device(s) available: ${details.join(", ")}`
328135
+ };
328136
+ }));
328137
+ return results;
328138
+ }
328139
+ function checkAndroid() {
328140
+ const results = [];
328141
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
328142
+ results.push(check("ANDROID_HOME", () => ({
328143
+ ok: !!androidHome && existsSync2(androidHome),
328144
+ detail: androidHome ? existsSync2(androidHome) ? androidHome : `${androidHome} (not found)` : "Not set. Set ANDROID_HOME to your Android SDK path."
328145
+ })));
328146
+ if (!androidHome || !existsSync2(androidHome)) return results;
328147
+ results.push(check("adb", () => {
328148
+ const version = run("adb version");
328149
+ if (!version) return { ok: false, detail: "Not found in PATH. Ensure Android platform-tools are installed." };
328150
+ const firstLine = version.split("\n")[0];
328151
+ return { ok: true, detail: firstLine };
328152
+ }));
328153
+ results.push(check("Appium uiautomator2 driver", () => {
328154
+ const drivers = run("appium driver list --installed --json");
328155
+ if (!drivers) return { ok: false, detail: "Appium not found. Run: npm i -g appium" };
328156
+ try {
328157
+ const parsed = JSON.parse(drivers);
328158
+ const hasUia2 = parsed?.uiautomator2 || typeof parsed === "object" && Object.keys(parsed).some((k) => k.toLowerCase().includes("uiautomator2"));
328159
+ return {
328160
+ ok: !!hasUia2,
328161
+ detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
328162
+ };
328163
+ } catch {
328164
+ const hasUia2 = drivers.toLowerCase().includes("uiautomator2");
328165
+ return {
328166
+ ok: hasUia2,
328167
+ detail: hasUia2 ? "Installed" : "Not installed. Run: appium driver install uiautomator2"
328168
+ };
328169
+ }
328170
+ }));
328171
+ results.push(check("Android Devices", () => {
328172
+ const output = run("adb devices");
328173
+ if (!output) return { ok: false, detail: "adb devices failed" };
328174
+ const lines = output.split("\n").filter((l) => l.includes(" device"));
328175
+ const emulators = lines.filter((l) => l.startsWith("emulator-"));
328176
+ const realDevices = lines.filter((l) => !l.startsWith("emulator-") && l.includes(" device"));
328177
+ const total = emulators.length + realDevices.length;
328178
+ if (total === 0) {
328179
+ return {
328180
+ ok: false,
328181
+ detail: "No devices found. Start an emulator or connect a real device via USB with USB debugging enabled."
328182
+ };
328183
+ }
328184
+ const details = [];
328185
+ if (emulators.length > 0) details.push(`${emulators.length} emulator(s)`);
328186
+ if (realDevices.length > 0) details.push(`${realDevices.length} real device(s)`);
328187
+ return {
328188
+ ok: true,
328189
+ detail: `${total} device(s) available: ${details.join(", ")}`
328190
+ };
328191
+ }));
328192
+ return results;
328193
+ }
328194
+ function checkTunnel() {
328195
+ const version = run("ngrok version") || run("ngrok --version");
328196
+ if (!version) {
328197
+ return [{
328198
+ name: "ngrok",
328199
+ ok: false,
328200
+ detail: "Not installed (only needed for --tunnel with an HTTPS dashboard). Install: brew install ngrok && ngrok config add-authtoken <token>"
328201
+ }];
328202
+ }
328203
+ const match = version.match(/(\d+)\.\d+\.\d+/);
328204
+ const major = match ? parseInt(match[1], 10) : 0;
328205
+ return [{
328206
+ name: "ngrok",
328207
+ ok: major >= 3,
328208
+ detail: major >= 3 ? `${version} (configure an authtoken with: ngrok config add-authtoken <token>)` : `${version} \u2014 ngrok v3+ is recommended (brew install ngrok)`
328209
+ }];
328210
+ }
328211
+ function runDoctor() {
328212
+ const web = checkWeb();
328213
+ const ios = checkIOS();
328214
+ const android = checkAndroid();
328215
+ const tunnel = checkTunnel();
328216
+ const allPassed = [...web, ...ios, ...android].every((c) => c.ok);
328217
+ return { web, ios, android, tunnel, allPassed };
328218
+ }
328219
+ function printDoctorReport(report) {
328220
+ console.log("");
328221
+ console.log(`${C.bold}${C.cyan}\u{1F3E5} validate.qa Doctor${C.reset}`);
328222
+ console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
328223
+ const printSection = (title, checks) => {
328224
+ console.log("");
328225
+ console.log(` ${C.bold}${title}:${C.reset}`);
328226
+ for (const c of checks) {
328227
+ const icon = c.ok ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`;
328228
+ console.log(` ${icon} ${c.name}: ${c.detail}`);
328229
+ }
328230
+ };
328231
+ printSection("Web Testing", report.web);
328232
+ printSection("iOS Testing", report.ios);
328233
+ printSection("Android Testing", report.android);
328234
+ printSection("Tunnel (optional \u2014 only for --tunnel / HTTPS dashboard)", report.tunnel);
328235
+ console.log("");
328236
+ if (report.allPassed) {
328237
+ console.log(` ${C.green}\u2705 All checks passed - ready for web + mobile testing${C.reset}`);
328238
+ } else {
328239
+ const failCount = [...report.web, ...report.ios, ...report.android].filter((c) => !c.ok).length;
328240
+ console.log(` ${C.yellow}\u26A0 ${failCount} check(s) need attention${C.reset}`);
328241
+ }
328242
+ console.log("");
328243
+ }
328244
+
328000
328245
  // src/runner.ts
328001
328246
  init_dist();
328002
328247
  var import_runner_core11 = __toESM(require_dist2());
@@ -328086,7 +328331,7 @@ var browserPool = new import_runner_core6.BrowserPool({
328086
328331
  maxContextsPerBrowser: envInt("MAX_CONTEXTS_PER_BROWSER", DEFAULT_MAX_CONTEXTS_PER_BROWSER),
328087
328332
  retireAfterContexts: 30
328088
328333
  });
328089
- var C = {
328334
+ var C2 = {
328090
328335
  reset: "\x1B[0m",
328091
328336
  bold: "\x1B[1m",
328092
328337
  green: "\x1B[32m",
@@ -328096,24 +328341,24 @@ var C = {
328096
328341
  gray: "\x1B[90m"
328097
328342
  };
328098
328343
  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}`)
328344
+ info: (msg) => console.log(`${C2.cyan}\u2139${C2.reset} ${msg}`),
328345
+ ok: (msg) => console.log(`${C2.green}\u2713${C2.reset} ${msg}`),
328346
+ fail: (msg) => console.log(`${C2.red}\u2717${C2.reset} ${msg}`),
328347
+ warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
328348
+ dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
328104
328349
  };
328105
328350
  var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
328106
328351
  function getRunsDir() {
328107
- const dir = process.env.RUNS_DIR || join3(homedir2(), ".validate.qa", "runs");
328108
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
328352
+ const dir = process.env.RUNS_DIR || join4(homedir3(), ".validate.qa", "runs");
328353
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
328109
328354
  return dir;
328110
328355
  }
328111
328356
  function authStatePath(projectId) {
328112
- return join3(getRunsDir(), `auth-state-${projectId || "default"}.json`);
328357
+ return join4(getRunsDir(), `auth-state-${projectId || "default"}.json`);
328113
328358
  }
328114
328359
  function getValidAuthState(projectId) {
328115
328360
  const path = authStatePath(projectId);
328116
- if (!existsSync2(path)) return void 0;
328361
+ if (!existsSync3(path)) return void 0;
328117
328362
  const age = Date.now() - statSync(path).mtimeMs;
328118
328363
  return age < AUTH_MAX_AGE_MS ? path : void 0;
328119
328364
  }
@@ -329056,9 +329301,9 @@ ${finalVerifyResult.logs ?? ""}`;
329056
329301
  (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
329302
  );
329058
329303
  if (phaseLines.length > 0) {
329059
- console.log(`${C.gray}\u2500\u2500 Heal summary \u2500\u2500${C.reset}`);
329304
+ console.log(`${C2.gray}\u2500\u2500 Heal summary \u2500\u2500${C2.reset}`);
329060
329305
  for (const line of phaseLines) console.log(line);
329061
- console.log(`${C.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C.reset}`);
329306
+ console.log(`${C2.gray}\u2500\u2500 End summary \u2500\u2500\u2500${C2.reset}`);
329062
329307
  }
329063
329308
  }
329064
329309
  log[result2.passed ? "ok" : "fail"](
@@ -329597,11 +329842,18 @@ ${finalVerifyResult.logs ?? ""}`;
329597
329842
  }
329598
329843
  try {
329599
329844
  if (isNativeMobile) {
329600
- const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : "ANDROID";
329601
329845
  const mobileTarget = ctx.target;
329602
329846
  if (!mobileTarget || !mobileTarget.appId) {
329603
329847
  throw new Error("Native mobile discovery requires a target with an appId (bundleId/appPackage).");
329604
329848
  }
329849
+ const targetPlatform = mobileTarget.platform;
329850
+ const platform3 = mobileMedium === "IOS_NATIVE" ? "IOS" : mobileMedium === "ANDROID_NATIVE" ? "ANDROID" : targetPlatform === "IOS" || targetPlatform === "ANDROID" ? targetPlatform : null;
329851
+ if (!platform3) {
329852
+ throw new Error("Native mobile discovery requires medium IOS_NATIVE/ANDROID_NATIVE or target.platform IOS/ANDROID.");
329853
+ }
329854
+ if (mobileMedium === "IOS_NATIVE" && targetPlatform === "ANDROID" || mobileMedium === "ANDROID_NATIVE" && targetPlatform === "IOS") {
329855
+ throw new Error("Native mobile discovery medium does not match target.platform.");
329856
+ }
329605
329857
  if (getBridgeState().hasActiveSession) {
329606
329858
  throw new Error("CONFIG_ISSUE: the shared mobile device is busy with an active recording session; mobile discovery will retry once it ends.");
329607
329859
  }
@@ -330030,9 +330282,9 @@ ${finalVerifyResult.logs ?? ""}`;
330030
330282
  } else {
330031
330283
  log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
330032
330284
  if (result.logs) {
330033
- console.log(`${C.gray}\u2500\u2500 Runner logs \u2500\u2500${C.reset}`);
330285
+ console.log(`${C2.gray}\u2500\u2500 Runner logs \u2500\u2500${C2.reset}`);
330034
330286
  console.log(result.logs);
330035
- console.log(`${C.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C.reset}`);
330287
+ console.log(`${C2.gray}\u2500\u2500 End logs \u2500\u2500\u2500\u2500\u2500${C2.reset}`);
330036
330288
  }
330037
330289
  }
330038
330290
  } catch (err) {
@@ -330054,20 +330306,24 @@ ${finalVerifyResult.logs ?? ""}`;
330054
330306
  }
330055
330307
  async function startRunner(opts) {
330056
330308
  const { serverUrl, authToken, projectId, pollIntervalMs = 2e3, runnerMode = "playwright-native", enableMobile = false, enableMobileTunnel = false } = opts;
330309
+ process.env.SERVER_URL = serverUrl;
330310
+ process.env.AUTH_TOKEN = authToken;
330311
+ process.env.RUNNER_TOKEN = authToken;
330312
+ if (projectId) process.env.PROJECT_ID = projectId;
330057
330313
  const headers = buildHeaders(authToken, projectId);
330058
330314
  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}`);
330315
+ console.log(`${C2.bold}${C2.cyan}\u{1F399}\uFE0F validate.qa Local Runner${C2.reset}`);
330316
+ console.log(`${C2.gray}${"\u2500".repeat(40)}${C2.reset}`);
330061
330317
  log.info(`Server: ${serverUrl}`);
330062
330318
  log.info(`Project: ${projectId}`);
330063
330319
  log.info(`Mode: \u{1F3AD} Playwright Native${enableMobile ? " + \u{1F4F1} Mobile" : ""}${enableMobile && enableMobileTunnel ? " \u{1F310} Tunnel" : ""}`);
330064
330320
  log.info(`Interval: ${pollIntervalMs}ms`);
330065
330321
  if (process.env.DISCOVERY_PROMPT_V3 === "true") {
330066
- log.ok("Discovery: V3 forced (DISCOVERY_PROMPT_V3=true)");
330322
+ log.ok("Discovery prompt: V3 (per-turn rebuild) \u2014 DISCOVERY_PROMPT_V3=true");
330067
330323
  } else if (process.env.DISCOVERY_PROMPT_V3 === "false") {
330068
- log.ok("Discovery: V2.5 forced (DISCOVERY_PROMPT_V3=false)");
330324
+ log.ok("Discovery prompt: V2.5 (full transcript + control brief) \u2014 DISCOVERY_PROMPT_V3=false");
330069
330325
  } else {
330070
- log.ok("Discovery: auto mode (Grok -> V2.5, others -> V3)");
330326
+ log.ok("Discovery prompt: V3 (per-turn rebuild, default) \u2014 set DISCOVERY_PROMPT_V3=false for V2.5");
330071
330327
  }
330072
330328
  if ((0, import_runner_core5.hasUsableChromeProfile)()) {
330073
330329
  log.ok(`Browser: Chrome with user profile (cookies/logins persist)`);
@@ -330079,8 +330335,8 @@ async function startRunner(opts) {
330079
330335
  const devices = discoverAllDevices();
330080
330336
  const capabilities = {
330081
330337
  web: true,
330082
- ios: devices.some((d) => d.platform === "IOS"),
330083
- android: devices.some((d) => d.platform === "ANDROID")
330338
+ ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
330339
+ android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
330084
330340
  };
330085
330341
  log.info(`\u{1F4F1} Mobile: iOS ${capabilities.ios ? "\u2713" : "\u2717"} | Android ${capabilities.android ? "\u2713" : "\u2717"} | ${devices.length} device(s)`);
330086
330342
  try {
@@ -330141,6 +330397,33 @@ async function startRunner(opts) {
330141
330397
  const startedAt = Date.now();
330142
330398
  const MAX_BACKOFF_MS = 6e4;
330143
330399
  let consecutivePollErrors = 0;
330400
+ let cachedDoctor = null;
330401
+ const DOCTOR_TTL_MS = 5 * 60 * 1e3;
330402
+ let doctorRefreshInFlight = false;
330403
+ const refreshDoctorInBackground = () => {
330404
+ if (doctorRefreshInFlight) return;
330405
+ doctorRefreshInFlight = true;
330406
+ setImmediate(() => {
330407
+ try {
330408
+ const report = runDoctor();
330409
+ cachedDoctor = { report, at: Date.now() };
330410
+ } catch (err) {
330411
+ log.dim(` [doctor] refresh failed: ${err instanceof Error ? err.message : String(err)}`);
330412
+ } finally {
330413
+ doctorRefreshInFlight = false;
330414
+ }
330415
+ });
330416
+ };
330417
+ const getCachedDoctor = () => {
330418
+ if (!enableMobile) return void 0;
330419
+ if (!cachedDoctor) {
330420
+ refreshDoctorInBackground();
330421
+ return void 0;
330422
+ }
330423
+ const now = Date.now();
330424
+ if (now - cachedDoctor.at >= DOCTOR_TTL_MS) refreshDoctorInBackground();
330425
+ return cachedDoctor.report;
330426
+ };
330144
330427
  log.info(`Concurrency: ${RESOURCE_GOVERNOR_OPTIONS.adaptive ? `adaptive (${RESOURCE_GOVERNOR_OPTIONS.maxUnits} weighted units, hard cap ${MAX_CONCURRENT})` : `static (hard cap ${MAX_CONCURRENT})`}`);
330145
330428
  const poolSize = envInt("BROWSER_POOL_SIZE", DEFAULT_BROWSER_POOL_SIZE);
330146
330429
  if (poolSize > 0) {
@@ -330249,7 +330532,11 @@ async function startRunner(opts) {
330249
330532
  const sendHeartbeat = async () => {
330250
330533
  try {
330251
330534
  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 };
330535
+ const capabilities = enableMobile ? {
330536
+ web: true,
330537
+ ios: devices.some((d) => d.platform === "IOS" && d.isAvailable && d.state === "BOOTED"),
330538
+ android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
330539
+ } : { web: true, ios: false, android: false };
330253
330540
  const bridge = enableMobile ? getBridgeState() : void 0;
330254
330541
  const resourceStatus = (0, import_runner_core11.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
330255
330542
  const processInventory = await (0, import_runner_core11.getBrowserProcessInventory)();
@@ -330280,7 +330567,10 @@ async function startRunner(opts) {
330280
330567
  resourceGovernor: resourceStatus.governor,
330281
330568
  processInventory,
330282
330569
  aiQueue: (0, import_runner_core11.getRunnerAIQueueStats)(),
330283
- liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat()
330570
+ liveBrowsers: import_runner_core10.liveBrowserRegistry.snapshotForHeartbeat(),
330571
+ // Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
330572
+ // Cached & cheap — see getCachedDoctor above.
330573
+ doctor: getCachedDoctor()
330284
330574
  }),
330285
330575
  signal: AbortSignal.timeout(1e4)
330286
330576
  });
@@ -330480,219 +330770,6 @@ async function startRunner(opts) {
330480
330770
  }
330481
330771
  }
330482
330772
 
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
330773
  // src/cli.ts
330697
330774
  var pkg = require_package4();
330698
330775
  var program = new Command();