UNPKG

@wdio/cli

Version:
3,275 lines • 121 kB
var __defProp = Object.defineProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};

// src/index.ts
import "dotenv/config";

// src/launcher.ts
import exitHook from "async-exit-hook";
import { resolve as resolve2 } from "import-meta-resolve";
import logger3 from "@wdio/logger";
import { validateConfig } from "@wdio/config";
import { ConfigParser as ConfigParser2 } from "@wdio/config/node";
import { initializePlugin, initializeLauncherService, sleep, enableFileLogging } from "@wdio/utils";
import { setupDriver, setupBrowser } from "@wdio/utils/node";

// src/interface.ts
import { EventEmitter } from "node:events";
import chalk2, { supportsColor } from "chalk";
import logger2 from "@wdio/logger";
import { SnapshotManager } from "@vitest/snapshot/manager";

// src/utils.ts
import fs2 from "node:fs/promises";
import util, { promisify } from "node:util";
import path2, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { execSync, spawn } from "node:child_process";
import ejs from "ejs";
import chalk from "chalk";
import inquirer from "inquirer";
import pickBy from "lodash.pickby";
import logger from "@wdio/logger";
import readDir from "recursive-readdir";
import { $ } from "execa";
import { readPackageUp } from "read-pkg-up";
import { resolve } from "import-meta-resolve";
import { SevereServiceError } from "webdriverio";
import { ConfigParser } from "@wdio/config/node";
import { CAPABILITY_KEYS } from "@wdio/protocols";

// src/install.ts
import { execa } from "execa";
var installCommand = {
  npm: "install",
  pnpm: "add",
  yarn: "add",
  bun: "install"
};
var devFlag = {
  npm: "--save-dev",
  pnpm: "--save-dev",
  yarn: "--dev",
  bun: "--dev"
};
async function installPackages(cwd, packages, dev) {
  const pm = detectPackageManager();
  const devParam = dev ? devFlag[pm] : "";
  console.log("\n");
  const p = execa(pm, [installCommand[pm], ...packages, devParam], {
    cwd,
    stdout: process.stdout,
    stderr: process.stderr
  });
  const { stdout, stderr, exitCode } = await p;
  if (exitCode !== 0) {
    const cmd = getInstallCommand(pm, packages, dev);
    const customError = `\u26A0\uFE0F An unknown error happened! Please retry installing dependencies via "${cmd}"

Error: ${stderr || stdout || "unknown"}`;
    console.error(customError);
    return false;
  }
  return true;
}
function getInstallCommand(pm, packages, dev) {
  const devParam = dev ? devFlag[pm] : "";
  return `${pm} ${installCommand[pm]} ${packages.join(" ")} ${devParam}`;
}

// src/constants.ts
import fs from "node:fs";
import path from "node:path";
import module from "node:module";
import { HOOK_DEFINITION } from "@wdio/utils";
var require2 = module.createRequire(import.meta.url);
var pkgJSON = require2("../package.json");
var pkg = pkgJSON;
var CLI_EPILOGUE = `Documentation: https://webdriver.io
@wdio/cli (v${pkg.version})`;
var CONFIG_HELPER_INTRO = `
===============================
\u{1F916} WDIO Configuration Wizard \u{1F9D9}
===============================
`;
var SUPPORTED_COMMANDS = ["run", "install", "config", "repl"];
var PMs = ["npm", "yarn", "pnpm", "bun"];
var SUPPORTED_CONFIG_FILE_EXTENSION = ["js", "ts", "mjs", "mts", "cjs", "cts"];
var configHelperSuccessMessage = ({ projectRootDir, runScript, extraInfo = "" }) => `
\u{1F916} Successfully setup project at ${projectRootDir} \u{1F389}

Join our Discord Community Server and instantly find answers to your issues or queries. Or just join and say hi \u{1F44B}!
  \u{1F517} https://discord.webdriver.io

Visit the project on GitHub to report bugs \u{1F41B} or raise feature requests \u{1F4A1}:
  \u{1F517} https://github.com/webdriverio/webdriverio
${extraInfo}
To run your tests, execute:
$ cd ${projectRootDir}
$ npm run ${runScript}
`;
var CONFIG_HELPER_SERENITY_BANNER = `
Learn more about Serenity/JS:
  \u{1F517} https://serenity-js.org/
  \u{1F517} https://serenity-js.org/handbook/test-runners/webdriverio/
`;
var DEPENDENCIES_INSTALLATION_MESSAGE = `
To install dependencies, execute:
%s
`;
var ANDROID_CONFIG = {
  platformName: "Android",
  automationName: "UiAutomator2",
  deviceName: "Test"
};
var IOS_CONFIG = {
  platformName: "iOS",
  automationName: "XCUITest",
  deviceName: "iPhone Simulator"
};
var SUPPORTED_PACKAGES = {
  runner: [
    { name: "E2E Testing - of Web or Mobile Applications", value: "@wdio/local-runner$--$local$--$e2e" },
    { name: "Component or Unit Testing - in the browser\n    > https://webdriver.io/docs/component-testing", value: "@wdio/browser-runner$--$browser$--$component" },
    { name: "Desktop Testing - of Electron Applications\n    > https://webdriver.io/docs/desktop-testing/electron", value: "@wdio/local-runner$--$local$--$electron" },
    { name: "Desktop Testing - of MacOS Applications\n    > https://webdriver.io/docs/desktop-testing/macos", value: "@wdio/local-runner$--$local$--$macos" },
    { name: "VS Code Extension Testing\n    > https://webdriver.io/docs/vscode-extension-testing", value: "@wdio/local-runner$--$local$--$vscode" },
    { name: "Roku Testing - of OTT apps running on RokuOS\n    > https://webdriver.io/docs/wdio-roku-service", value: "@wdio/local-runner$--$local$--$roku" }
  ],
  framework: [
    { name: "Mocha (https://mochajs.org/)", value: "@wdio/mocha-framework$--$mocha" },
    { name: "Mocha with Serenity/JS (https://serenity-js.org/)", value: "@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$mocha" },
    { name: "Jasmine (https://jasmine.github.io/)", value: "@wdio/jasmine-framework$--$jasmine" },
    { name: "Jasmine with Serenity/JS (https://serenity-js.org/)", value: "@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$jasmine" },
    { name: "Cucumber (https://cucumber.io/)", value: "@wdio/cucumber-framework$--$cucumber" },
    { name: "Cucumber with Serenity/JS (https://serenity-js.org/)", value: "@serenity-js/webdriverio$--$@serenity-js/webdriverio$--$cucumber" }
  ],
  reporter: [
    { name: "spec", value: "@wdio/spec-reporter$--$spec", checked: true },
    { name: "dot", value: "@wdio/dot-reporter$--$dot" },
    { name: "junit", value: "@wdio/junit-reporter$--$junit" },
    { name: "allure", value: "@wdio/allure-reporter$--$allure" },
    { name: "sumologic", value: "@wdio/sumologic-reporter$--$sumologic" },
    { name: "concise", value: "@wdio/concise-reporter$--$concise" },
    { name: "json", value: "@wdio/json-reporter$--$json" },
    // external
    { name: "reportportal", value: "wdio-reportportal-reporter$--$reportportal" },
    { name: "video", value: "wdio-video-reporter$--$video" },
    { name: "cucumber-json", value: "wdio-cucumberjs-json-reporter$--$cucumberjs-json" },
    { name: "mochawesome", value: "wdio-mochawesome-reporter$--$mochawesome" },
    { name: "timeline", value: "wdio-timeline-reporter$--$timeline" },
    { name: "html-nice", value: "wdio-html-nice-reporter$--$html-nice" },
    { name: "slack", value: "@moroo/wdio-slack-reporter$--$slack" },
    { name: "teamcity", value: "wdio-teamcity-reporter$--$teamcity" },
    { name: "delta", value: "@delta-reporter/wdio-delta-reporter-service$--$delta" },
    { name: "testrail", value: "@wdio/testrail-reporter$--$testrail" },
    { name: "light", value: "wdio-light-reporter$--$light" },
    { name: "wdio-json-html-reporter", value: "wdio-json-html-reporter$--$jsonhtml" }
  ],
  plugin: [
    { name: "wait-for: utilities that provide functionalities to wait for certain conditions till a defined task is complete.\n   > https://www.npmjs.com/package/wdio-wait-for", value: "wdio-wait-for$--$wait-for" },
    { name: "angular-component-harnesses: support for Angular component test harnesses\n   > https://www.npmjs.com/package/@badisi/wdio-harness", value: "@badisi/wdio-harness$--$harness" },
    { name: "Testing Library: utilities that encourage good testing practices laid down by dom-testing-library.\n   > https://testing-library.com/docs/webdriverio-testing-library/intro", value: "@testing-library/webdriverio$--$testing-library" }
  ],
  service: [
    // internal or community driver services
    { name: "visual", value: "@wdio/visual-service$--$visual" },
    { name: "vite", value: "wdio-vite-service$--$vite" },
    { name: "nuxt", value: "wdio-nuxt-service$--$nuxt" },
    { name: "firefox-profile", value: "@wdio/firefox-profile-service$--$firefox-profile" },
    { name: "gmail", value: "wdio-gmail-service$--$gmail" },
    { name: "sauce", value: "@wdio/sauce-service$--$sauce" },
    { name: "testingbot", value: "@wdio/testingbot-service$--$testingbot" },
    { name: "browserstack", value: "@wdio/browserstack-service$--$browserstack" },
    { name: "lighthouse", value: "@wdio/lighthouse-service$--$lighthouse" },
    { name: "vscode", value: "wdio-vscode-service$--$vscode" },
    { name: "electron", value: "wdio-electron-service$--$electron" },
    { name: "appium", value: "@wdio/appium-service$--$appium" },
    // external
    { name: "eslinter-service", value: "wdio-eslinter-service$--$eslinter" },
    { name: "lambdatest", value: "wdio-lambdatest-service$--$lambdatest" },
    { name: "zafira-listener", value: "wdio-zafira-listener-service$--$zafira-listener" },
    { name: "reportportal", value: "wdio-reportportal-service$--$reportportal" },
    { name: "docker", value: "wdio-docker-service$--$docker" },
    { name: "ui5", value: "wdio-ui5-service$--$ui5" },
    { name: "wiremock", value: "wdio-wiremock-service$--$wiremock" },
    { name: "ng-apimock", value: "wdio-ng-apimock-service$--$ng-apimock" },
    { name: "slack", value: "wdio-slack-service$--$slack" },
    { name: "cucumber-viewport-logger", value: "wdio-cucumber-viewport-logger-service$--$cucumber-viewport-logger" },
    { name: "intercept", value: "wdio-intercept-service$--$intercept" },
    { name: "docker", value: "wdio-docker-service$--$docker" },
    { name: "novus-visual-regression", value: "wdio-novus-visual-regression-service$--$novus-visual-regression" },
    { name: "rerun", value: "wdio-rerun-service$--$rerun" },
    { name: "winappdriver", value: "wdio-winappdriver-service$--$winappdriver" },
    { name: "ywinappdriver", value: "wdio-ywinappdriver-service$--$ywinappdriver" },
    { name: "performancetotal", value: "wdio-performancetotal-service$--$performancetotal" },
    { name: "cleanuptotal", value: "wdio-cleanuptotal-service$--$cleanuptotal" },
    { name: "aws-device-farm", value: "wdio-aws-device-farm-service$--$aws-device-farm" },
    { name: "ms-teams", value: "wdio-ms-teams-service$--$ms-teams" },
    { name: "tesults", value: "wdio-tesults-service$--$tesults" },
    { name: "azure-devops", value: "@gmangiapelo/wdio-azure-devops-service$--$azure-devops" },
    { name: "google-Chat", value: "wdio-google-chat-service$--$google-chat" },
    { name: "qmate-service", value: "@sap_oss/wdio-qmate-service$--$qmate-service" },
    { name: "robonut", value: "wdio-robonut-service$--$robonut" },
    { name: "qunit", value: "wdio-qunit-service$--$qunit" },
    { name: "roku", value: "wdio-roku-service$--$roku" }
  ]
};
var SUPPORTED_BROWSER_RUNNER_PRESETS = [
  { name: "Lit (https://lit.dev/)", value: "$--$" },
  { name: "Vue.js (https://vuejs.org/)", value: "@vitejs/plugin-vue$--$vue" },
  { name: "Svelte (https://svelte.dev/)", value: "@sveltejs/vite-plugin-svelte$--$svelte" },
  { name: "SolidJS (https://www.solidjs.com/)", value: "vite-plugin-solid$--$solid" },
  { name: "StencilJS (https://stenciljs.com/)", value: "$--$stencil" },
  { name: "React (https://reactjs.org/)", value: "@vitejs/plugin-react$--$react" },
  { name: "Preact (https://preactjs.com/)", value: "@preact/preset-vite$--$preact" },
  { name: "Other", value: null }
];
var TESTING_LIBRARY_PACKAGES = {
  react: "@testing-library/react",
  preact: "@testing-library/preact",
  vue: "@testing-library/vue",
  svelte: "@testing-library/svelte",
  solid: "solid-testing-library"
};
var BackendChoice = /* @__PURE__ */ ((BackendChoice2) => {
  BackendChoice2["Local"] = "On my local machine";
  BackendChoice2["Experitest"] = "In the cloud using Experitest";
  BackendChoice2["Saucelabs"] = "In the cloud using Sauce Labs";
  BackendChoice2["Browserstack"] = "In the cloud using BrowserStack";
  BackendChoice2["OtherVendors"] = "In the cloud using Testingbot or LambdaTest or a different service";
  BackendChoice2["Grid"] = "I have my own Selenium cloud";
  return BackendChoice2;
})(BackendChoice || {});
var ElectronBuildToolChoice = /* @__PURE__ */ ((ElectronBuildToolChoice2) => {
  ElectronBuildToolChoice2["ElectronForge"] = "Electron Forge (https://www.electronforge.io/)";
  ElectronBuildToolChoice2["ElectronBuilder"] = "electron-builder (https://www.electron.build/)";
  ElectronBuildToolChoice2["SomethingElse"] = "Something else";
  return ElectronBuildToolChoice2;
})(ElectronBuildToolChoice || {});
var ProtocolOptions = /* @__PURE__ */ ((ProtocolOptions2) => {
  ProtocolOptions2["HTTPS"] = "https";
  ProtocolOptions2["HTTP"] = "http";
  return ProtocolOptions2;
})(ProtocolOptions || {});
var RegionOptions = /* @__PURE__ */ ((RegionOptions2) => {
  RegionOptions2["US"] = "us";
  RegionOptions2["EU"] = "eu";
  return RegionOptions2;
})(RegionOptions || {});
var E2E_ENVIRONMENTS = [
  { name: "Web - web applications in the browser", value: "web" },
  { name: "Mobile - native, hybrid and mobile web apps, on Android or iOS", value: "mobile" }
];
var MOBILE_ENVIRONMENTS = [
  { name: "Android - native, hybrid and mobile web apps, tested on emulators and real devices\n    > using UiAutomator2 (https://www.npmjs.com/package/appium-uiautomator2-driver)", value: "android" },
  { name: "iOS - applications on iOS, iPadOS, and tvOS\n    > using XCTest (https://appium.github.io/appium-xcuitest-driver)", value: "ios" }
];
var BROWSER_ENVIRONMENTS = [
  { name: "Chrome", value: "chrome", checked: true },
  { name: "Firefox", value: "firefox" },
  { name: "Safari", value: "safari" },
  { name: "Microsoft Edge", value: "MicrosoftEdge" }
];
function isBrowserRunner(answers) {
  return answers.runner === SUPPORTED_PACKAGES.runner[1].value;
}
function usesSerenity(answers) {
  return answers.framework.includes("serenity-js");
}
function getTestingPurpose(answers) {
  return convertPackageHashToObject(answers.runner).purpose;
}
var isNuxtProject = [
  path.join(process.cwd(), "nuxt.config.js"),
  path.join(process.cwd(), "nuxt.config.ts"),
  path.join(process.cwd(), "nuxt.config.mjs"),
  path.join(process.cwd(), "nuxt.config.mts")
].map((p) => {
  try {
    fs.accessSync(p);
    return true;
  } catch {
    return false;
  }
}).some(Boolean);
function selectDefaultService(serviceNames) {
  serviceNames = Array.isArray(serviceNames) ? serviceNames : [serviceNames];
  return SUPPORTED_PACKAGES.service.filter(({ name }) => serviceNames.includes(name)).map(({ value }) => value);
}
function prioServiceOrderFor(serviceNamesParam) {
  const serviceNames = Array.isArray(serviceNamesParam) ? serviceNamesParam : [serviceNamesParam];
  let services = SUPPORTED_PACKAGES.service;
  for (const serviceName of serviceNames) {
    const index = services.findIndex(({ name }) => name === serviceName);
    services = [services[index], ...services.slice(0, index), ...services.slice(index + 1)];
  }
  return services;
}
var QUESTIONNAIRE = [{
  type: "list",
  name: "runner",
  message: "What type of testing would you like to do?",
  choices: SUPPORTED_PACKAGES.runner
}, {
  type: "list",
  name: "preset",
  message: "Which framework do you use for building components?",
  choices: SUPPORTED_BROWSER_RUNNER_PRESETS,
  // only ask if there are more than 1 runner to pick from
  when: (
    /* istanbul ignore next */
    isBrowserRunner
  )
}, {
  type: "confirm",
  name: "installTestingLibrary",
  message: "Do you like to use Testing Library (https://testing-library.com/) as test utility?",
  default: true,
  // only ask if there are more than 1 runner to pick from
  when: (
    /* istanbul ignore next */
    (answers) => isBrowserRunner(answers) && /**
     * Only show if Testing Library has an add-on for framework
     */
    answers.preset && TESTING_LIBRARY_PACKAGES[convertPackageHashToObject(answers.preset).short]
  )
}, {
  type: "list",
  name: "electronBuildTool",
  message: "Which tool are you using to build your Electron app?",
  choices: Object.values(ElectronBuildToolChoice),
  when: (
    /* instanbul ignore next */
    (answers) => getTestingPurpose(answers) === "electron"
  )
}, {
  type: "input",
  name: "electronAppBinaryPath",
  message: "What is the path to the binary of your built Electron app?",
  when: (
    /* istanbul ignore next */
    (answers) => getTestingPurpose(answers) === "electron" && answers.electronBuildTool === "Something else" /* SomethingElse */
  )
}, {
  type: "list",
  name: "backend",
  message: "Where is your automation backend located?",
  choices: Object.values(BackendChoice),
  when: (
    /* instanbul ignore next */
    (answers) => getTestingPurpose(answers) === "e2e"
  )
}, {
  type: "list",
  name: "e2eEnvironment",
  message: "Which environment you would like to automate?",
  choices: E2E_ENVIRONMENTS,
  default: "web",
  when: (
    /* istanbul ignore next */
    (answers) => getTestingPurpose(answers) === "e2e"
  )
}, {
  type: "list",
  name: "mobileEnvironment",
  message: "Which mobile environment you'd like to automate?",
  choices: MOBILE_ENVIRONMENTS,
  when: (
    /* instanbul ignore next */
    (answers) => getTestingPurpose(answers) === "e2e" && answers.e2eEnvironment === "mobile"
  )
}, {
  type: "checkbox",
  name: "browserEnvironment",
  message: "With which browser should we start?",
  choices: BROWSER_ENVIRONMENTS,
  when: (
    /* instanbul ignore next */
    (answers) => getTestingPurpose(answers) === "e2e" && answers.e2eEnvironment === "web"
  )
}, {
  type: "input",
  name: "hostname",
  message: "What is the host address of that cloud service?",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend && answers.backend.indexOf("different service") > -1
  )
}, {
  type: "input",
  name: "port",
  message: "What is the port on which that service is running?",
  default: "80",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend && answers.backend.indexOf("different service") > -1
  )
}, {
  type: "input",
  name: "expEnvAccessKey",
  message: "Access key from Experitest Cloud",
  default: "EXPERITEST_ACCESS_KEY",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using Experitest" /* Experitest */
  )
}, {
  type: "input",
  name: "expEnvHostname",
  message: "Environment variable for cloud url",
  default: "example.experitest.com",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using Experitest" /* Experitest */
  )
}, {
  type: "input",
  name: "expEnvPort",
  message: "Environment variable for port",
  default: "443",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using Experitest" /* Experitest */
  )
}, {
  type: "list",
  name: "expEnvProtocol",
  message: "Choose a protocol for environment variable",
  default: "https" /* HTTPS */,
  choices: Object.values(ProtocolOptions),
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using Experitest" /* Experitest */ && answers.expEnvPort !== "80" && answers.expEnvPort !== "443"
  )
}, {
  type: "input",
  name: "env_user",
  message: "Environment variable for username",
  default: "LT_USERNAME",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend && answers.backend.indexOf("LambdaTest") > -1 && answers.hostname.indexOf("lambdatest.com") > -1
  )
}, {
  type: "input",
  name: "env_key",
  message: "Environment variable for access key",
  default: "LT_ACCESS_KEY",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend && answers.backend.indexOf("LambdaTest") > -1 && answers.hostname.indexOf("lambdatest.com") > -1
  )
}, {
  type: "input",
  name: "env_user",
  message: "Environment variable for username",
  default: "BROWSERSTACK_USERNAME",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using BrowserStack" /* Browserstack */
  )
}, {
  type: "input",
  name: "env_key",
  message: "Environment variable for access key",
  default: "BROWSERSTACK_ACCESS_KEY",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using BrowserStack" /* Browserstack */
  )
}, {
  type: "input",
  name: "env_user",
  message: "Environment variable for username",
  default: "SAUCE_USERNAME",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */
  )
}, {
  type: "input",
  name: "env_key",
  message: "Environment variable for access key",
  default: "SAUCE_ACCESS_KEY",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */
  )
}, {
  type: "list",
  name: "region",
  message: "In which region do you want to run your Sauce Labs tests in?",
  choices: Object.values(RegionOptions),
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */
  )
}, {
  type: "confirm",
  name: "useSauceConnect",
  message: "Are you testing a local application and need Sauce Connect to be set-up?\nRead more on Sauce Connect at: https://docs.saucelabs.com/secure-connections/#sauce-connect-proxy",
  default: isNuxtProject,
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */ && !isNuxtProject
  )
}, {
  type: "input",
  name: "hostname",
  message: "What is the IP or URI to your Selenium standalone or grid server?",
  default: "localhost",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend && answers.backend.toString().indexOf("own Selenium cloud") > -1
  )
}, {
  type: "input",
  name: "port",
  message: "What is the port which your Selenium standalone or grid server is running on?",
  default: "4444",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend && answers.backend.toString().indexOf("own Selenium cloud") > -1
  )
}, {
  type: "input",
  name: "path",
  message: "What is the path to your browser driver or grid server?",
  default: "/",
  when: (
    /* istanbul ignore next */
    (answers) => answers.backend && answers.backend.toString().indexOf("own Selenium cloud") > -1
  )
}, {
  type: "list",
  name: "framework",
  message: "Which framework do you want to use?",
  choices: (
    /* instanbul ignore next */
    (answers) => {
      if (isBrowserRunner(answers)) {
        return SUPPORTED_PACKAGES.framework.slice(0, 1);
      }
      if (getTestingPurpose(answers) === "electron") {
        return SUPPORTED_PACKAGES.framework.filter(
          ({ value }) => !value.startsWith("@serenity-js")
        );
      }
      return SUPPORTED_PACKAGES.framework;
    }
  )
}, {
  type: "confirm",
  name: "isUsingTypeScript",
  message: "Do you want to use Typescript to write tests?",
  when: (
    /* istanbul ignore next */
    (answers) => {
      if (answers.preset?.includes("stencil")) {
        return false;
      }
      return true;
    }
  ),
  default: (
    /* istanbul ignore next */
    (answers) => answers.preset?.includes("stencil") || detectCompiler(answers)
  )
}, {
  type: "confirm",
  name: "generateTestFiles",
  message: "Do you want WebdriverIO to autogenerate some test files?",
  default: true,
  when: (
    /* istanbul ignore next */
    (answers) => {
      if (["vscode", "electron", "macos"].includes(getTestingPurpose(answers)) && answers.framework.includes("cucumber")) {
        return false;
      }
      return true;
    }
  )
}, {
  type: "input",
  name: "specs",
  message: "What should be the location of your spec files?",
  default: (
    /* istanbul ignore next */
    (answers) => {
      const pattern = isBrowserRunner(answers) ? "src/**/*.test" : "test/specs/**/*";
      return getDefaultFiles(answers, pattern);
    }
  ),
  when: (
    /* istanbul ignore next */
    (answers) => answers.generateTestFiles && /(mocha|jasmine)/.test(answers.framework)
  )
}, {
  type: "input",
  name: "specs",
  message: "What should be the location of your feature files?",
  default: (answers) => getDefaultFiles(answers, "features/**/*.feature"),
  when: (
    /* istanbul ignore next */
    (answers) => answers.generateTestFiles && answers.framework.includes("cucumber")
  )
}, {
  type: "input",
  name: "stepDefinitions",
  message: "What should be the location of your step definitions?",
  default: (answers) => getDefaultFiles(answers, "features/step-definitions/steps"),
  when: (
    /* istanbul ignore next */
    (answers) => answers.generateTestFiles && answers.framework.includes("cucumber")
  )
}, {
  type: "confirm",
  name: "usePageObjects",
  message: "Do you want to use page objects (https://martinfowler.com/bliki/PageObject.html)?",
  default: true,
  when: (
    /* istanbul ignore next */
    (answers) => answers.generateTestFiles && /**
     * page objects aren't common for component testing
     */
    !isBrowserRunner(answers) && /**
     * and also not needed when running VS Code tests since the service comes with
     * its own page object implementation, nor when running Electron or MacOS tests
     */
    !["vscode", "electron", "macos"].includes(getTestingPurpose(answers)) && /**
     * Serenity/JS generates Lean Page Objects by default, so there's no need to ask about it
     * See https://serenity-js.org/handbook/web-testing/page-objects-pattern/
     */
    !usesSerenity(answers)
  )
}, {
  type: "input",
  name: "pages",
  message: "Where are your page objects located?",
  default: (
    /* istanbul ignore next */
    (answers) => answers.framework.match(/(mocha|jasmine)/) ? getDefaultFiles(answers, "test/pageobjects/**/*") : getDefaultFiles(answers, "features/pageobjects/**/*")
  ),
  when: (
    /* istanbul ignore next */
    (answers) => answers.generateTestFiles && answers.usePageObjects
  )
}, {
  type: "input",
  name: "serenityLibPath",
  message: "What should be the location of your Serenity/JS Screenplay Pattern library?",
  default: (
    /* istanbul ignore next */
    async (answers) => {
      const projectRootDir = await getProjectRoot(answers);
      const specsDir = path.resolve(projectRootDir, path.dirname(answers.specs || "").replace(/\*\*$/, ""));
      return path.resolve(specsDir, "..", "serenity");
    }
  ),
  when: (
    /* istanbul ignore next */
    (answers) => answers.generateTestFiles && usesSerenity(answers)
  )
}, {
  type: "checkbox",
  name: "reporters",
  message: "Which reporter do you want to use?",
  choices: SUPPORTED_PACKAGES.reporter
}, {
  type: "checkbox",
  name: "plugins",
  message: "Do you want to add a plugin to your test setup?",
  choices: SUPPORTED_PACKAGES.plugin,
  default: []
}, {
  type: "confirm",
  name: "includeVisualTesting",
  message: "Would you like to include Visual Testing to your setup? For more information see https://webdriver.io/docs/visual-testing!",
  default: false,
  when: (
    /* istanbul ignore next */
    (answers) => {
      return ["e2e", "component"].includes(getTestingPurpose(answers));
    }
  )
}, {
  type: "checkbox",
  name: "services",
  message: "Do you want to add a service to your test setup?",
  choices: (answers) => {
    const services = [];
    if (answers.backend === "In the cloud using BrowserStack" /* Browserstack */) {
      services.push("browserstack");
    } else if (answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */) {
      services.push("sauce");
    }
    if (answers.e2eEnvironment === "mobile") {
      services.push("appium");
    }
    if (getTestingPurpose(answers) === "e2e" && isNuxtProject) {
      services.push("nuxt");
    }
    if (getTestingPurpose(answers) === "vscode") {
      return [SUPPORTED_PACKAGES.service.find(({ name }) => name === "vscode")];
    } else if (getTestingPurpose(answers) === "electron") {
      return [SUPPORTED_PACKAGES.service.find(({ name }) => name === "electron")];
    } else if (getTestingPurpose(answers) === "macos") {
      return [SUPPORTED_PACKAGES.service.find(({ name }) => name === "appium")];
    } else if (getTestingPurpose(answers) === "roku") {
      return [SUPPORTED_PACKAGES.service.find(({ name }) => name === "roku")];
    }
    return prioServiceOrderFor(services);
  },
  default: (answers) => {
    const defaultServices = [];
    if (answers.backend === "In the cloud using BrowserStack" /* Browserstack */) {
      defaultServices.push("browserstack");
    } else if (answers.backend === "In the cloud using Sauce Labs" /* Saucelabs */) {
      defaultServices.push("sauce");
    }
    if (answers.e2eEnvironment === "mobile" || getTestingPurpose(answers) === "macos") {
      defaultServices.push("appium");
    }
    if (getTestingPurpose(answers) === "vscode") {
      defaultServices.push("vscode");
    } else if (getTestingPurpose(answers) === "electron") {
      defaultServices.push("electron");
    } else if (getTestingPurpose(answers) === "roku") {
      defaultServices.push("roku");
    }
    if (isNuxtProject) {
      defaultServices.push("nuxt");
    }
    if (answers.includeVisualTesting) {
      defaultServices.push("visual");
    }
    return selectDefaultService(defaultServices);
  }
}, {
  type: "input",
  name: "outputDir",
  message: "In which directory should the xunit reports get stored?",
  default: "./",
  when: (
    /* istanbul ignore next */
    (answers) => answers.reporters.includes("junit")
  )
}, {
  type: "input",
  name: "outputDir",
  message: "In which directory should the json reports get stored?",
  default: "./",
  when: (
    /* istanbul ignore next */
    (answers) => answers.reporters.includes("json")
  )
}, {
  type: "input",
  name: "outputDir",
  message: "In which directory should the mochawesome json reports get stored?",
  default: "./",
  when: (
    /* istanbul ignore next */
    (answers) => answers.reporters.includes("mochawesome")
  )
}, {
  type: "confirm",
  name: "npmInstall",
  message: () => `Do you want me to run \`${detectPackageManager()} install\``,
  default: true
}];
var SUPPORTED_SNAPSHOTSTATE_OPTIONS = ["all", "new", "none"];
var COMMUNITY_PACKAGES_WITH_TS_SUPPORT = [
  "wdio-electron-service",
  "wdio-vscode-service",
  "wdio-nuxt-service",
  "wdio-vite-service",
  "wdio-gmail-service",
  "wdio-roku-service"
];
var TESTRUNNER_DEFAULTS = {
  /**
   * Define specs for test execution. You can either specify a glob
   * pattern to match multiple files at once or wrap a glob or set of
   * paths into an array to run them within a single worker process.
   */
  specs: {
    type: "object",
    validate: (param) => {
      if (!Array.isArray(param)) {
        throw new Error('the "specs" option needs to be a list of strings');
      }
    }
  },
  /**
   * exclude specs from test execution
   */
  exclude: {
    type: "object",
    validate: (param) => {
      if (!Array.isArray(param)) {
        throw new Error('the "exclude" option needs to be a list of strings');
      }
    }
  },
  /**
   * key/value definition of suites (named by key) and a list of specs as value
   * to specify a specific set of tests to execute
   */
  suites: {
    type: "object"
  },
  /**
   * Project root directory path.
   */
  rootDir: {
    type: "string"
  },
  /**
   * If you only want to run your tests until a specific amount of tests have failed use
   * bail (default is 0 - don't bail, run all tests).
   */
  bail: {
    type: "number",
    default: 0
  },
  /**
   * supported test framework by wdio testrunner
   */
  framework: {
    type: "string"
  },
  /**
   * capabilities of WebDriver sessions
   */
  capabilities: {
    type: "object",
    validate: (param) => {
      if (!Array.isArray(param)) {
        if (typeof param === "object") {
          return true;
        }
        throw new Error('the "capabilities" options needs to be an object or a list of objects');
      }
      for (const option of param) {
        if (typeof option === "object") {
          continue;
        }
        throw new Error("expected every item of a list of capabilities to be of type object");
      }
      return true;
    },
    required: true
  },
  /**
   * list of reporters to use, a reporter can be either a string or an object with
   * reporter options, e.g.:
   * [
   *  'dot',
   *  {
   *    name: 'spec',
   *    outputDir: __dirname + '/reports'
   *  }
   * ]
   */
  reporters: {
    type: "object",
    validate: (param) => {
      if (!Array.isArray(param)) {
        throw new Error('the "reporters" options needs to be a list of strings');
      }
      const isValidReporter = (option) => typeof option === "string" || typeof option === "function";
      for (const option of param) {
        if (isValidReporter(option)) {
          continue;
        }
        if (Array.isArray(option) && typeof option[1] === "object" && isValidReporter(option[0])) {
          continue;
        }
        throw new Error(
          'a reporter should be either a string in the format "wdio-<reportername>-reporter" or a function/class. Please see the docs for more information on custom reporters (https://webdriver.io/docs/customreporter)'
        );
      }
      return true;
    }
  },
  /**
   * set of WDIO services to use
   */
  services: {
    type: "object",
    validate: (param) => {
      if (!Array.isArray(param)) {
        throw new Error('the "services" options needs to be a list of strings and/or arrays');
      }
      for (const option of param) {
        if (!Array.isArray(option)) {
          if (typeof option === "string") {
            continue;
          }
          throw new Error('the "services" options needs to be a list of strings and/or arrays');
        }
      }
      return true;
    },
    default: []
  },
  /**
   * Node arguments to specify when launching child processes
   */
  execArgv: {
    type: "object",
    validate: (param) => {
      if (!Array.isArray(param)) {
        throw new Error('the "execArgv" options needs to be a list of strings');
      }
    },
    default: []
  },
  /**
   * amount of instances to be allowed to run in total
   */
  maxInstances: {
    type: "number"
  },
  /**
   * amount of instances to be allowed to run per capability
   */
  maxInstancesPerCapability: {
    type: "number"
  },
  /**
   * whether or not testrunner should inject `browser`, `$` and `$$` as
   * global environment variables
   */
  injectGlobals: {
    type: "boolean"
  },
  /**
   * Set to true if you want to update your snapshots.
   */
  updateSnapshots: {
    type: "string",
    default: SUPPORTED_SNAPSHOTSTATE_OPTIONS[1],
    validate: (param) => {
      if (param && !SUPPORTED_SNAPSHOTSTATE_OPTIONS.includes(param)) {
        throw new Error(`the "updateSnapshots" options needs to be one of "${SUPPORTED_SNAPSHOTSTATE_OPTIONS.join('", "')}"`);
      }
    }
  },
  /**
   * Overrides default snapshot path. For example, to store snapshots next to test files.
   */
  resolveSnapshotPath: {
    type: "function",
    validate: (param) => {
      if (param && typeof param !== "function") {
        throw new Error('the "resolveSnapshotPath" options needs to be a function');
      }
    }
  },
  /**
   * The number of times to retry the entire specfile when it fails as a whole
   */
  specFileRetries: {
    type: "number",
    default: 0
  },
  /**
   * Delay in seconds between the spec file retry attempts
   */
  specFileRetriesDelay: {
    type: "number",
    default: 0
  },
  /**
   * Whether or not retried spec files should be retried immediately or deferred to the end of the queue
   */
  specFileRetriesDeferred: {
    type: "boolean",
    default: true
  },
  /**
   * whether or not print the log output grouped by test files
   */
  groupLogsByTestSpec: {
    type: "boolean",
    default: false
  },
  /**
   * list of strings to watch of `wdio` command is called with `--watch` flag
   */
  filesToWatch: {
    type: "object",
    validate: (param) => {
      if (!Array.isArray(param)) {
        throw new Error('the "filesToWatch" option needs to be a list of strings');
      }
    }
  },
  shard: {
    type: "object",
    validate: (param) => {
      if (typeof param !== "object") {
        throw new Error('the "shard" options needs to be an object');
      }
      const p = param;
      if (typeof p.current !== "number" || typeof p.total !== "number") {
        throw new Error('the "shard" option needs to have "current" and "total" properties with number values');
      }
      if (p.current < 0 || p.current > p.total) {
        throw new Error('the "shard.current" value has to be between 0 and "shard.total"');
      }
    }
  },
  /**
   * hooks
   */
  onPrepare: HOOK_DEFINITION,
  onWorkerStart: HOOK_DEFINITION,
  onWorkerEnd: HOOK_DEFINITION,
  before: HOOK_DEFINITION,
  beforeSession: HOOK_DEFINITION,
  beforeSuite: HOOK_DEFINITION,
  beforeHook: HOOK_DEFINITION,
  beforeTest: HOOK_DEFINITION,
  afterTest: HOOK_DEFINITION,
  afterHook: HOOK_DEFINITION,
  afterSuite: HOOK_DEFINITION,
  afterSession: HOOK_DEFINITION,
  after: HOOK_DEFINITION,
  onComplete: HOOK_DEFINITION,
  onReload: HOOK_DEFINITION,
  beforeAssertion: HOOK_DEFINITION,
  afterAssertion: HOOK_DEFINITION
};
var WORKER_GROUPLOGS_MESSAGES = {
  normalExit: (cid) => `
***** List of steps of WorkerID=[${cid}] *****`,
  exitWithError: (cid) => `
***** List of steps of WorkerID=[${cid}] that preceded the error above *****`
};

// src/templates/EjsHelpers.ts
var EjsHelpers = class {
  useTypeScript;
  useEsm;
  constructor(config) {
    this.useTypeScript = config.useTypeScript ?? false;
    this.useEsm = config.useEsm ?? false;
  }
  if(condition, trueValue, falseValue = "") {
    return condition ? trueValue : falseValue;
  }
  ifTs = (trueValue, falseValue = "") => this.if(this.useTypeScript, trueValue, falseValue);
  ifEsm = (trueValue, falseValue = "") => this.if(this.useEsm, trueValue, falseValue);
  param(name, type) {
    return this.useTypeScript ? `${name}: ${type}` : name;
  }
  returns(type) {
    return this.useTypeScript ? `: ${type}` : "";
  }
  import(exports, moduleId) {
    const individualExports = exports.split(",").map((id) => id.trim());
    const imports = this.useTypeScript ? individualExports : individualExports.filter((id) => !id.startsWith("type "));
    if (!imports.length) {
      return "";
    }
    const modulePath = this.modulePathFrom(moduleId);
    return this.useEsm || this.useTypeScript ? `import { ${imports.join(", ")} } from '${modulePath}'` : `const { ${imports.join(", ")} } = require('${modulePath}')`;
  }
  modulePathFrom(moduleId) {
    if (!(moduleId.startsWith(".") && this.useEsm)) {
      return moduleId;
    }
    if (moduleId.endsWith("/") && this.useEsm) {
      return moduleId + "index.js";
    }
    return moduleId + ".js";
  }
  export(keyword, name) {
    if (this.useTypeScript) {
      return `export ${keyword} ${name}`;
    }
    if (this.useEsm) {
      return `export ${keyword} ${name}`;
    }
    if (["class", "function"].includes(keyword)) {
      return `module.exports.${name} = ${keyword} ${name}`;
    }
    return `module.exports.${name}`;
  }
};

// src/utils.ts
var log = logger("@wdio/cli:utils");
var __dirname = dirname(fileURLToPath(import.meta.url));
var NPM_COMMAND = /^win/.test(process.platform) ? "npm.cmd" : "npm";
var VERSION_REGEXP = /(\d+)\.(\d+)\.(\d+)-(alpha|beta|)\.(\d+)\+(.+)/g;
var TEMPLATE_ROOT_DIR = path2.join(__dirname, "templates", "exampleFiles");
var renderFile = promisify(ejs.renderFile);
var HookError = class extends SevereServiceError {
  origin;
  constructor(message, origin) {
    super(message);
    this.origin = origin;
  }
};
async function runServiceHook(launcher, hookName, ...args) {
  const start = Date.now();
  return Promise.all(launcher.map(async (service) => {
    try {
      if (typeof service[hookName] === "function") {
        await service[hookName](...args);
      }
    } catch (err) {
      const message = `A service failed in the '${hookName}' hook
${err.stack}

`;
      if (err instanceof SevereServiceError || err.name === "SevereServiceError") {
        return { status: "rejected", reason: message, origin: hookName };
      }
      log.error(`${message}Continue...`);
    }
  })).then((results) => {
    if (launcher.length) {
      log.debug(`Finished to run "${hookName}" hook in ${Date.now() - start}ms`);
    }
    const rejectedHooks = results.filter((p) => p && p.status === "rejected");
    if (rejectedHooks.length) {
      return Promise.reject(new HookError(`
${rejectedHooks.map((p) => p && p.reason).join()}

Stopping runner...`, hookName));
    }
  });
}
async function runLauncherHook(hook, ...args) {
  if (typeof hook === "function") {
    hook = [hook];
  }
  const catchFn = (e) => {
    log.error(`Error in hook: ${e.stack}`);
    if (e instanceof SevereServiceError) {
      throw new HookError(e.message, hook[0].name);
    }
  };
  return Promise.all(hook.map((hook2) => {
    try {
      return hook2(...args);
    } catch (err) {
      return catchFn(err);
    }
  })).catch(catchFn);
}
async function runOnCompleteHook(onCompleteHook, config, capabilities, exitCode, results) {
  if (typeof onCompleteHook === "function") {
    onCompleteHook = [onCompleteHook];
  }
  return Promise.all(onCompleteHook.map(async (hook) => {
    try {
      await hook(exitCode, config, capabilities, results);
      return 0;
    } catch (err) {
      log.error(`Error in onCompleteHook: ${err.stack}`);
      if (err instanceof SevereServiceError) {
        throw new HookError(err.message, "onComplete");
      }
      return 1;
    }
  }));
}
function getRunnerName(caps = {}) {
  let runner = caps.browserName || caps.platformName || caps["appium:platformName"] || caps["appium:appPackage"] || caps["appium:appWaitActivity"] || caps["appium:app"];
  if (!runner) {
    runner = Object.values(caps).length === 0 || Object.values(caps).some((cap) => !cap.capabilities) ? "undefined" : "MultiRemote";
  }
  return runner;
}
function buildNewConfigArray(str, type, change) {
  const newStr = str.split(`${type}s: `)[1].replace(/'/g, "");
  const newArray = newStr.match(/(\w*)/gmi)?.filter((e) => !!e).concat([change]) || [];
  return str.replace("// ", "").replace(
    new RegExp(`(${type}s: )((.*\\s*)*)`),
    `$1[${newArray.map((e) => `'${e}'`)}]`
  );
}
function buildNewConfigString(str, type, change) {
  return str.replace(new RegExp(`(${type}: )('\\w*')`), `$1'${change}'`);
}
function findInConfig(config, type) {
  let regexStr = `[\\/\\/]*[\\s]*${type}s: [\\s]*\\[([\\s]*['|"]\\w*['|"],*)*[\\s]*\\]`;
  if (type === "framework") {
    regexStr = `[\\/\\/]*[\\s]*${type}: ([\\s]*['|"]\\w*['|"])`;
  }
  const regex = new RegExp(regexStr, "gmi");
  return config.match(regex);
}
function replaceConfig(config, type, name) {
  if (type === "framework") {
    return buildNewConfigString(config, type, name);
  }
  const match = findInConfig(config, type);
  if (!match || match.length === 0) {
    return;
  }
  const text = match.pop() || "";
  return config.replace(text, buildNewConfigArray(text, type, name));
}
function addServiceDeps(names, packages, update = false) {
  if (names.some(({ short }) => short === "appium")) {
    const result = execSync("appium --version || echo APPIUM_MISSING", { stdio: "pipe" }).toString().trim();
    if (result === "APPIUM_MISSING") {
      packages.push("appium");
    } else if (update) {
      console.log(
        "\n=======",
        "\nUsing globally installed appium",
        result,
        "\nPlease add the following to your wdio.conf.js:",
        "\nappium: { command: 'appium' }",
        "\n=======\n"
      );
    }
  }
}
function convertPackageHashToObject(pkg2, hash = "$--$") {
  const [p, short, purpose] = pkg2.split(hash);
  return { package: p, short, purpose };
}
function getSerenityPackages(answers) {
  const framework = convertPackageHashToObject(answers.framework);
  if (framework.package !== "@serenity-js/webdriverio") {
    return [];
  }
  const packages = {
    cucumber: [
      "@cucumber/cucumber",
      "@serenity-js/cucumber"
    ],
    mocha: [
      "@serenity-js/mocha",
      "mocha"
    ],
    jasmine: [
      "@serenity-js/jasmine",
      "jasmine"
    ],
    common: [
      "@serenity-js/assertions",
      "@serenity-js/console-reporter",
      "@serenity-js/core",
      "@serenity-js/rest",
      "@serenity-js/serenity-bdd",
      "@serenity-js/web",
      "npm-failsafe",
      "rimraf"
    ]
  };
  if (answers.isUsingTypeScript) {
    packages.mocha.push("@types/mocha");
    packages.jasmine.push("@types/jasmine");
    packages.common.push("@types/node");
  }
  return [
    ...packages[framework.purpose],
    ...packages.common
  ].filter(Boolean).sort();
}
async function getCapabilities(arg) {
  const optionalCapabilites = {
    platformVersion: arg.platformVersion,
    udid: arg.udid,
    ...arg.deviceName && { deviceName: arg.deviceName }
  };
  if (/.*\.(apk|app|ipa)$/.test(arg.option)) {
    return {
      capabilities: {
        app: arg.option,
        ...arg.option.endsWith("apk") ? ANDROID_CONFIG : IOS_CONFIG,
        ...optionalCapabilites
      }
    };
  } else if (/android/.test(arg.option)) {
    return { capabilities: { browserName: "Chrome", ...ANDROID_CONFIG, ...optionalCapabilites } };
  } else if (/ios/.test(arg.option)) {
    return { capabilities: { browserName: "Safari", ...IOS_CONFIG, ...optionalCapabilites } };
  } else if (/(js|ts)$/.test(arg.option)) {
    const config = new ConfigParser(arg.option);
    try {
      await config.initialize();
    } catch (e) {
      throw Error(e.code === "MODULE_NOT_FOUND" ? `Config File not found: ${arg.option}` : `Could not parse ${arg.option}, failed with error: ${e.message}`);
    }
    if (typeof arg.capabilities === "undefined") {
      throw Error("Please provide index/named property of capability to use from the capabilities array/object in wdio config file");
    }
    let requiredCaps = config.getCapabilities();
    requiredCaps = // multi capabilities
    requiredCaps[parseInt(arg.capabilities, 10)] || // multiremote
    requiredCaps[arg.capabilities];
    const requiredW3CCaps = pickBy(requiredCaps, (_, key) => CAPABILITY_KEYS.includes(key) || key.includes(":"));
    if (!Object.keys(requiredW3CCaps).length) {
      throw Error(`No capability found in given config file with the provided capability indexed/named property: ${arg.capabilities}. Please check the capability in your wdio config file.`);
    }
    return { capabilities: { ...requiredW3CCaps } };
  }
  return { capabilities: { browserName: arg.option } };
}
async function detectCompiler(answers) {
  if (answers.createPackageJSON) {
    return false;
  }
  const root = await getProjectRoot(answers);
  const hasRootTSConfig = await fs2.access(path2.resolve(root, "tsconfig.json")).then(() => true, () => false);
  return hasRootTSConfig;
}
async function generateTestFiles(answers) {
  if (answers.serenityAdapter) {
    return generateSerenityExamples(answers);
  }
  if (answers.runner === "local") {
    return generateLocalRunnerTestFiles(answers);
  }
  return generateBrowserRunnerTestFiles(answers);
}
var TSX_BASED_FRAMEWORKS = ["react", "preact", "solid", "stencil"];
async function generateBrowserRunnerTestFiles(answers) {
  const isUsingFramework = typeof answers.preset === "string";
  const preset = getPreset(answers);
  const tplRootDir = path2.join(TEMPLATE_ROOT_DIR, "browser");
  await fs2.mkdir(answers.destSpecRootPath, { recursive: true });
  if (isUsingFramework) {
    const renderedCss = await renderFile(path2.join(tplRootDir, "Component.css.ejs"), { answers });
    await fs2.writeFile(path2.join(answers.destSpecRootPath, "Component.css"), renderedCss);
  }
  const testExt = `${answers.isUsingTypeScript ? "ts" : "js"}${TSX_BASED_FRAMEWORKS.includes(preset) ? "x" : ""}`;
  const fileExt = ["svelte", "vue"].includes(preset) ? preset : testExt;
  if (preset) {
    const componentOutFileName = `Component.${fileExt}`;
    const renderedComponent = await renderFile(path2.join(tplRootDir, `Component.${preset}.ejs`), { answers });
    await fs2.writeFile(path2.join(answers.destSpecRootPath, componentOutFileName), renderedComponent);
  }
  const componentFileName = preset ? `Component.${preset}.test.ejs` : "standalone.test.ejs";
  const renderedTest = await renderFile(path2.join(tplRootDir, componentFileName), { answers });
  await fs2.writeFile(path2.join(answers.destSpecRootPath, `Component.test.${testExt}`), renderedTest);
}
async function generateLocalRunnerTestFiles(answers) {
  const testFiles = answers.framework === "cucumber" ? [path2.join(TEMPLATE_ROOT_DIR, "cucumber")] : [path2.join(TEMPLATE_ROOT_DIR, "mochaJasmine")];
  if (answers.usePageObjects) {
    testFiles.push(path2.join(TEMPLATE_ROOT_DIR, "pageobjects"));
  }
  const files = (await Promise.all(testFiles.map((dirPath) => readDir(
    dirPath,
    [(file, stats) => !stats.isDirectory() && !(file.endsWith(".ejs") || file.endsWith(".feature"))]
  )))).reduce((cur, acc) => [...acc, ...cur], []);
  await Promise.all(files.map(async (file) => {
    const renderedTpl = await renderFile(file, { answers });
    const isJSX = answers.preset && TSX_BASED_FRAMEWORKS.includes(answers.preset);
    const fileEnding = (answers.isUsingTypeScript ? ".ts" : ".js") + (isJSX ? "x" : "");
    const destPath = (file.endsWith("page.js.ejs") ? path2.join(answers.destPageObjectRootPath, path2.basename(file)) : file.includes("step_definition") ? path2.join(answers.destStepRootPath, path2.basename(file)) : path2.join(answers.destSpecRootPath, path2.basename(file))).replace(/\.ejs$/, "").replace(/\.js$/, fileEnding);
    await fs2.mkdir(path2.dirname(destPath), { recursive: true });
    await fs2.writeFile(destPath, renderedTpl);
  }));
}
async function generateSerenityExamples(answers) {
  const templateDirectories = Object.entries({
    [answers.projectRootDir]: path2.join(TEMPLATE_ROOT_DIR, "serenity-js", "common", "config"),
    [answers.destSpecRootPath]: path2.join(TEMPLATE_ROOT_DIR, "serenity-js", answers.serenityAdapter),
    [answers.destSerenityLibRootPath]: path2.join(TEMPLATE_ROOT_DIR, "serenity-js", "common", "serenity")
  });
  await Promise.all(templateDirectories.map(async ([destinationRootDir, templateRootDir]) => {
    const pathsToTemplates = await readDir(templateRootDir);
    await Promise.all(pathsToTemplates.map(async (pathToTemplate) => {
      const extension = answers.isUsingTypeScript ? ".ts" : ".js";
      const destination = path2.join(destinationRootDir, path2.relative(templateRootDir, pathToTemplate)).replace(/\.ejs$/, "").replace(/\.ts$/, extension);
      const contents = await renderFile(
        pathToTemplate,
        { answers, _: new EjsHelpers({ useEsm: answers.esmSupport, useTypeScript: answers.isUsingTypeScript }) }
      );
      await fs2.mkdir(path2.dirname(destination), { recursive: true });
      await fs2.writeFile(destination, contents);
    }));
  }));
}
async function getAnswers(yes) {
  if (yes) {
    const ignoredQuestions = ["e2eEnvironment"];
    const filterdQuestionaire = QUESTIONNAIRE.filter((question) => !ignoredQuestions.includes(question.name));
    const answers2 = {};
    for (const question of filterdQuestionaire) {
      if (question.when && !question.when(answers2)) {
        continue;
      }
      Object.assign(answers2, {
        [question.name]: typeof question.default !== "undefined" ? typeof question.default === "function" ? await question.default(answers2) : question.default : question.choices && question.choices.length ? typeof question.choices === "function" ? question.choices(answers2)[0].value ? question.choices(answers2)[0].value : question.choices(answers2)[0] : question.choices[0].value ? question.type === "checkbox" ? [question.choices[0].value] : question.choices[0].value : question.choices[0] : {}
      });
    }
    answers2.isUsingTypeScript = await answers2.isUsingTypeScript;
    answers2.specs = await answers2.specs;
    answers2.pages = await answers2.pages;
    return answers2;
  }
  const projectProps = await getProjectProps(process.cwd());
  const isProjectExisting = Boolean(projectProps);
  const nameInPackageJsonIsNotCreateWdioDefault = projectProps?.packageJson?.name !== "my-new-project";
  const projectName = projectProps?.packageJson?.name ? ` named "${projectProps.packageJson.name}"` : "";
  const projectRootQuestions = [
    {
      type: "confirm",
      name: "createPackageJSON",
      default: true,
      message: `Couldn't find a package.json in "${process.cwd()}" or any of the parent directories, do you want to create one?`,
      when: !isProjectExisting
    },
    {
      type: "confirm",
      name: "projectRootCorrect",
      default: true,
      message: `A project${projectName} was detected at "${projectProps?.path}", correct?`,
      when: isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault
    },
    {
      type: "input",
      name: "projectRoot",
      message: "What is the project root for your test project?",
      default: projectProps?.path,
      // only ask if there are more than 1 runner to pick from
      when: (
        /* istanbul ignore next */
        (answers2) => isProjectExisting && nameInPackageJsonIsNotCreateWdioDefault && !answers2.projectRootCorrect
      )
    }
  ];
  const answers = await inquirer.prompt(projectRootQuestions);
  if (answers.createPackageJSON) {
    answers.projectRoot = process.cwd();
  }
  return inquirer.prompt(QUESTIONNAIRE, answers);
}
function generatePathfromAnswer(answers, projectRootDir) {
  return path2.resolve(
    projectRootDir,
    path2.dirname(answers) === "." ? path2.resolve(answers) : path2.dirname(answers)
  );
}
function getPathForFileGeneration(answers, projectRootDir) {
  const specAnswer = answers.specs || "";
  const stepDefinitionAnswer = answers.stepDefinitions || "";
  const pageObjectAnswer = answers.pages || "";
  const destSpecRootPath = generatePathfromAnswer(specAnswer, projectRootDir).replace(/\*\*$/, "");
  const destStepRootPath = generatePathfromAnswer(stepDefinitionAnswer, projectRootDir);
  const destPageObjectRootPath = answers.usePageObjects ? generatePathfromAnswer(pageObjectAnswer, projectRootDir).replace(/\*\*$/, "") : "";
  const destSerenityLibRootPath = usesSerenity(answers) ? path2.resolve(projectRootDir, answers.serenityLibPath || "serenity") : "";
  const relativePath = answers.generateTestFiles && answers.usePageObjects ? !(convertPackageHashToObject(answers.framework).short === "cucumber") ? path2.relative(destSpecRootPath, destPageObjectRootPath) : path2.relative(destStepRootPath, destPageObjectRootPath) : "";
  return {
    destSpecRootPath,
    destStepRootPath,
    destPageObjectRootPath,
    destSerenityLibRootPath,
    relativePath: relativePath.replaceAll(path2.sep, "/")
  };
}
async function getDefaultFiles(answers, pattern) {
  const rootdir = await getProjectRoot(answers);
  const presetPackage = convertPackageHashToObject(answers.preset || "");
  const isJSX = TSX_BASED_FRAMEWORKS.includes(presetPackage.short || "");
  const val = pattern.endsWith(".feature") ? path2.join(rootdir, pattern) : answers?.isUsingTypeScript ? `${path2.join(rootdir, pattern)}.ts${isJSX ? "x" : ""}` : `${path2.join(rootdir, pattern)}.js${isJSX ? "x" : ""}`;
  return val;
}
function specifyVersionIfNeeded(packagesToInstall, version, npmTag) {
  const { value } = version.matchAll(VERSION_REGEXP).next();
  const [major, minor, patch, tagName, build] = (value || []).slice(1, -1);
  return packagesToInstall.map((p) => {
    if (p.startsWith("@wdio") && p !== "@wdio/visual-service" || ["webdriver", "webdriverio"].includes(p)) {
      const tag = major && npmTag === "latest" ? `^${major}.${minor}.${patch}-${tagName}.${build}` : npmTag;
      return `${p}@${tag}`;
    }
    return p;
  });
}
async function getProjectProps(cwd = process.cwd()) {
  try {
    const { packageJson, path: packageJsonPath } = await readPackageUp({ cwd }) || {};
    if (!packageJson || !packageJsonPath) {
      return void 0;
    }
    return {
      esmSupported: packageJson.type === "module" || typeof packageJson.module === "string",
      packageJson,
      path: path2.dirname(packageJsonPath)
    };
  } catch {
    return void 0;
  }
}
function runProgram(command5, args, options) {
  const child = spawn(command5, args, { stdio: "inherit", ...options });
  return new Promise((resolve3, reject) => {
    let error;
    child.on("error", (e) => error = e);
    child.on("close", (code) => {
      if (code !== 0) {
        return reject(new Error(
          error && error.message || `Error calling: ${command5} ${args.join(" ")}`
        ));
      }
      resolve3();
    });
  });
}
async function createPackageJSON(parsedAnswers) {
  const packageJsonExists = await fs2.access(path2.resolve(process.cwd(), "package.json")).then(() => true, () => false);
  if (packageJsonExists) {
    return;
  }
  if (parsedAnswers.createPackageJSON === false) {
    if (!packageJsonExists) {
      console.log(`No WebdriverIO configuration found in "${parsedAnswers.wdioConfigPath}"`);
      return !process.env.WDIO_UNIT_TESTS && process.exit(0);
    }
    return;
  }
  if (parsedAnswers.createPackageJSON) {
    console.log(`Creating a ${chalk.bold("package.json")} for the directory...`);
    await fs2.writeFile(path2.resolve(process.cwd(), "package.json"), JSON.stringify({
      name: "webdriverio-tests",
      version: "0.0.0",
      private: true,
      license: "ISC",
      type: "module",
      dependencies: {},
      devDependencies: {}
    }, null, 2));
    console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
  }
}
var SEP = "\n- ";
async function npmInstall(parsedAnswers, npmTag) {
  const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service));
  const presetPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.preset || "");
  if (parsedAnswers.installTestingLibrary && TESTING_LIBRARY_PACKAGES[presetPackage.short]) {
    parsedAnswers.packagesToInstall.push(
      TESTING_LIBRARY_PACKAGES[presetPackage.short],
      "@testing-library/jest-dom"
    );
  }
  if (presetPackage.short === "solid") {
    parsedAnswers.packagesToInstall.push("solid-js");
  }
  if (parsedAnswers.includeVisualTesting) {
    parsedAnswers.packagesToInstall.push("@wdio/visual-service");
  }
  const preset = getPreset(parsedAnswers);
  if (preset === "lit") {
    parsedAnswers.packagesToInstall.push("lit");
  }
  if (preset === "stencil") {
    parsedAnswers.packagesToInstall.push("@stencil/core");
  }
  if (presetPackage.short === "react") {
    parsedAnswers.packagesToInstall.push("react");
    if (!parsedAnswers.installTestingLibrary) {
      parsedAnswers.packagesToInstall.push("react-dom");
    }
  }
  if (parsedAnswers.framework === "jasmine" && parsedAnswers.isUsingTypeScript) {
    parsedAnswers.packagesToInstall.push("@types/jasmine");
  }
  if (parsedAnswers.purpose === "macos") {
    parsedAnswers.packagesToInstall.push("appium-mac2-driver");
  }
  if (parsedAnswers.mobileEnvironment === "android") {
    parsedAnswers.packagesToInstall.push("appium-uiautomator2-driver");
  }
  if (parsedAnswers.mobileEnvironment === "ios") {
    parsedAnswers.packagesToInstall.push("appium-xcuitest-driver");
  }
  addServiceDeps(servicePackages, parsedAnswers.packagesToInstall);
  parsedAnswers.packagesToInstall = specifyVersionIfNeeded(parsedAnswers.packagesToInstall, pkg.version, npmTag);
  const cwd = await getProjectRoot(parsedAnswers);
  const pm = detectPackageManager();
  if (parsedAnswers.npmInstall) {
    console.log(`Installing packages using ${pm}:${SEP}${parsedAnswers.packagesToInstall.join(SEP)}`);
    const success = await installPackages(cwd, parsedAnswers.packagesToInstall, true);
    if (success) {
      console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
    }
  } else {
    const installationCommand = getInstallCommand(pm, parsedAnswers.packagesToInstall, true);
    console.log(util.format(DEPENDENCIES_INSTALLATION_MESSAGE, installationCommand));
  }
}
function detectPackageManager() {
  if (!process.env.npm_config_user_agent) {
    return "npm";
  }
  const detectedPM = process.env.npm_config_user_agent.split("/")[0].toLowerCase();
  const matchedPM = PMs.find((pm) => pm.toLowerCase() === detectedPM);
  return matchedPM || "npm";
}
async function setupTypeScript(parsedAnswers) {
  if (!parsedAnswers.isUsingTypeScript) {
    return;
  }
  if (parsedAnswers.hasRootTSConfig) {
    return;
  }
  console.log("Setting up TypeScript...");
  const frameworkPackage = convertPackageHashToObject(parsedAnswers.rawAnswers.framework);
  const servicePackages = parsedAnswers.rawAnswers.services.map((service) => convertPackageHashToObject(service));
  const serenityTypes = parsedAnswers.serenityAdapter === "jasmine" ? ["jasmine"] : [];
  const types = [
    "node",
    ...parsedAnswers.framework === "jasmine" ? ["jasmine"] : [],
    "@wdio/globals/types",
    ...parsedAnswers.framework === "jasmine" ? ["expect-webdriverio/jasmine"] : ["expect-webdriverio"],
    ...parsedAnswers.serenityAdapter ? serenityTypes : [frameworkPackage.package],
    ...parsedAnswers.runner === "browser" ? ["@wdio/browser-runner"] : [],
    ...servicePackages.filter((service) => (
      /**
       * given that we know that all "official" services have
       * typescript support we only include them
       */
      service.package.startsWith("@wdio") || /**
       * also include community maintained packages with known
       * support for TypeScript
       */
      COMMUNITY_PACKAGES_WITH_TS_SUPPORT.includes(service.package)
    )).map((service) => service.package)
  ];
  const preset = getPreset(parsedAnswers);
  const config = {
    compilerOptions: {
      // compiler
      moduleResolution: "node",
      module: !parsedAnswers.esmSupport ? "commonjs" : "ESNext",
      target: "es2022",
      lib: ["es2022", "dom"],
      types,
      skipLibCheck: true,
      // bundler
      noEmit: true,
      allowImportingTsExtensions: true,
      resolveJsonModule: true,
      isolatedModules: true,
      // linting
      strict: true,
      noUnusedLocals: true,
      noUnusedParameters: true,
      noFallthroughCasesInSwitch: true,
      ...Object.assign(
        preset === "lit" ? {
          experimentalDecorators: true,
          useDefineForClassFields: false
        } : {},
        preset === "react" ? {
          jsx: "react-jsx"
        } : {},
        preset === "preact" ? {
          jsx: "react-jsx",
          jsxImportSource: "preact"
        } : {},
        preset === "solid" ? {
          jsx: "preserve",
          jsxImportSource: "solid-js"
        } : {},
        preset === "stencil" ? {
          experimentalDecorators: true,
          jsx: "react",
          jsxFactory: "h",
          jsxFragmentFactory: "Fragment"
        } : {}
      )
    },
    include: preset === "svelte" ? ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] : preset === "vue" ? ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] : ["test", "wdio.conf.ts"]
  };
  if (parsedAnswers.framework === "cucumber") {
    config.include.push("features");
  }
  await fs2.mkdir(path2.dirname(parsedAnswers.tsConfigFilePath), { recursive: true });
  await fs2.writeFile(
    parsedAnswers.tsConfigFilePath,
    JSON.stringify(config, null, 4)
  );
  console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
}
function getPreset(parsedAnswers) {
  const isUsingFramework = typeof parsedAnswers.preset === "string";
  return isUsingFramework ? parsedAnswers.preset || "lit" : "";
}
async function createWDIOConfig(parsedAnswers) {
  try {
    console.log("Creating a WebdriverIO config file...");
    const tplPath = path2.resolve(__dirname, "templates", "wdio.conf.tpl.ejs");
    const renderedTpl = await renderFile(tplPath, {
      answers: parsedAnswers,
      _: new EjsHelpers({ useEsm: parsedAnswers.esmSupport, useTypeScript: parsedAnswers.isUsingTypeScript })
    });
    await fs2.writeFile(parsedAnswers.wdioConfigPath, renderedTpl);
    console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
    if (parsedAnswers.generateTestFiles) {
      console.log("Autogenerating test files...");
      await generateTestFiles(parsedAnswers);
      console.log(chalk.green(chalk.bold("\u2714 Success!\n")));
    }
  } catch (err) {
    throw new Error(`\u26A0\uFE0F Couldn't write config file: ${err.stack}`);
  }
}
async function getProjectRoot(parsedAnswers) {
  if (parsedAnswers?.createPackageJSON && parsedAnswers.projectRoot) {
    return parsedAnswers.projectRoot;
  }
  const root = (await getProjectProps())?.path;
  if (!root) {
    throw new Error("Could not find project root directory with a package.json");
  }
  return !parsedAnswers || parsedAnswers.projectRootCorrect ? root : parsedAnswers.projectRoot || process.cwd();
}
async function createWDIOScript(parsedAnswers) {
  const rootDir = await getProjectRoot(parsedAnswers);
  const pathToWdioConfig = `./${path2.join(".", parsedAnswers.wdioConfigPath.replace(rootDir, ""))}`;
  const wdioScripts = {
    "wdio": `wdio run ${pathToWdioConfig}`
  };
  const serenityScripts = {
    "serenity": "failsafe serenity:clean wdio serenity:report",
    "serenity:clean": "rimraf target",
    "wdio": `wdio run ${pathToWdioConfig}`,
    "serenity:report": "serenity-bdd run"
  };
  const scripts = parsedAnswers.serenityAdapter ? serenityScripts : wdioScripts;
  for (const [script, command5] of Object.entries(scripts)) {
    const args = ["pkg", "set", `scripts.${script}=${command5}`];
    try {
      console.log(`Adding ${chalk.bold(`"${script}"`)} script to package.json`);
      await runProgram(NPM_COMMAND, args, { cwd: parsedAnswers.projectRootDir });
    } catch (err) {
      const [preArgs, scriptPath] = args.join(" ").split("=");
      console.error(
        `\u26A0\uFE0F  Couldn't add script to package.json: "${err.message}", you can add it manually by running:

	${NPM_COMMAND} ${preArgs}="${scriptPath}"`
      );
      return false;
    }
  }
  console.log(chalk.green(chalk.bold("\u2714 Success!")));
  return true;
}
async function runAppiumInstaller(parsedAnswers) {
  if (parsedAnswers.e2eEnvironment !== "mobile") {
    return;
  }
  const answer = await inquirer.prompt({
    name: "continueWithAppiumSetup",
    message: "Continue with Appium setup using appium-installer (https://github.com/AppiumTestDistribution/appium-installer)?",
    type: "confirm",
    default: true
  });
  if (!answer.continueWithAppiumSetup) {
    return console.log(
      "Ok! You can learn more about setting up mobile environments in the Appium docs at https://appium.io/docs/en/2.0/quickstart/"
    );
  }
  return $({ stdio: "inherit" })`npx appium-installer`;
}
var cucumberTypes = {
  paths: "array",
  backtrace: "boolean",
  dryRun: "boolean",
  forceExit: "boolean",
  failFast: "boolean",
  format: "array",
  formatOptions: "object",
  import: "array",
  language: "string",
  name: "array",
  order: "string",
  publish: "boolean",
  requireModule: "array",
  retry: "number",
  retryTagFilter: "string",
  strict: "boolean",
  tags: "string",
  worldParameters: "object",
  timeout: "number",
  scenarioLevelReporter: "boolean",
  tagsInTitle: "boolean",
  ignoreUndefinedDefinitions: "boolean",
  failAmbiguousDefinitions: "boolean",
  tagExpression: "string",
  profiles: "array",
  file: "string"
};
var mochaTypes = {
  require: "array",
  compilers: "array",
  allowUncaught: "boolean",
  asyncOnly: "boolean",
  bail: "boolean",
  checkLeaks: "boolean",
  delay: "boolean",
  fgrep: "string",
  forbidOnly: "boolean",
  forbidPending: "boolean",
  fullTrace: "boolean",
  global: "array",
  grep: "string",
  invert: "boolean",
  retries: "number",
  timeout: "number",
  ui: "string"
};
var jasmineTypes = {
  defaultTimeoutInterval: "number",
  helpers: "array",
  requires: "array",
  random: "boolean",
  seed: "string",
  failFast: "boolean",
  failSpecWithNoExpectations: "boolean",
  oneFailurePerSpec: "boolean",
  grep: "string",
  invertGrep: "boolean",
  cleanStack: "boolean",
  stopOnSpecFailure: "boolean",
  stopSpecOnExpectationFailure: "boolean",
  requireModule: "array"
};
function coerceOpts(types, opts) {
  for (const key in opts) {
    if (types[key] === "boolean" && typeof opts[key] === "string") {
      opts[key] = opts[key] === "true";
    } else if (types[key] === "number") {
      opts[key] = Number(opts[key]);
    } else if (types[key] === "array") {
      opts[key] = Array.isArray(opts[key]) ? opts[key] : [opts[key]];
    } else if (types[key] === "object" && typeof opts[key] === "string") {
      opts[key] = JSON.parse(opts[key]);
    }
  }
  return opts;
}
function coerceOptsFor(framework) {
  if (framework === "cucumber") {
    return coerceOpts.bind(null, cucumberTypes);
  } else if (framework === "mocha") {
    return coerceOpts.bind(null, mochaTypes);
  } else if (framework === "jasmine") {
    return coerceOpts.bind(null, jasmineTypes);
  }
  throw new Error(`Unsupported framework "${framework}"`);
}
var NodeVersion = /* @__PURE__ */ ((NodeVersion2) => {
  NodeVersion2[NodeVersion2["major"] = 0] = "major";
  NodeVersion2[NodeVersion2["minor"] = 1] = "minor";
  NodeVersion2[NodeVersion2["patch"] = 2] = "patch";
  return NodeVersion2;
})(NodeVersion || {});
function nodeVersion(type) {
  return process.versions.node.split(".").map(Number)[NodeVersion[type]];
}

// src/interface.ts
var log2 = logger2("@wdio/cli");
var EVENT_FILTER = ["sessionStarted", "sessionEnded", "finishedCommand", "ready", "workerResponse", "workerEvent"];
var WDIOCLInterface = class extends EventEmitter {
  constructor(_config, totalWorkerCnt, _isWatchMode = false) {
    super();
    this._config = _config;
    this.totalWorkerCnt = totalWorkerCnt;
    this._isWatchMode = _isWatchMode;
    this.hasAnsiSupport = supportsColor && supportsColor.hasBasic;
    this.totalWorkerCnt = totalWorkerCnt;
    this._isWatchMode = _isWatchMode;
    this._specFileRetries = _config.specFileRetries || 0;
    this._specFileRetriesDelay = _config.specFileRetriesDelay || 0;
    this.on("job:start", this.addJob.bind(this));
    this.on("job:end", this.clearJob.bind(this));
    this.setup();
    this.onStart();
  }
  #snapshotManager = new SnapshotManager({
    updateSnapshot: "new"
    // ignored in this context
  });
  hasAnsiSupport;
  result = {
    finished: 0,
    passed: 0,
    retries: 0,
    failed: 0
  };
  _jobs = /* @__PURE__ */ new Map();
  _specFileRetries;
  _specFileRetriesDelay;
  _skippedSpecs = 0;
  _inDebugMode = false;
  _start = /* @__PURE__ */ new Date();
  _messages = {
    reporter: {},
    debugger: {}
  };
  #hasShard() {
    return this._config.shard && this._config.shard.total !== 1;
  }
  setup() {
    this._jobs = /* @__PURE__ */ new Map();
    this._start = /* @__PURE__ */ new Date();
    this.result = {
      finished: 0,
      passed: 0,
      retries: 0,
      failed: 0
    };
    this._messages = {
      reporter: {},
      debugger: {}
    };
  }
  onStart() {
    const shardNote = this.#hasShard() ? ` (Shard ${this._config.shard.current} of ${this._config.shard.total})` : "";
    this.log(chalk2.bold(`
Execution of ${chalk2.blue(this.totalWorkerCnt)} workers${shardNote} started at`), this._start.toISOString());
    if (this._inDebugMode) {
      this.log(chalk2.bgYellow(chalk2.black("DEBUG mode enabled!")));
    }
    if (this._isWatchMode) {
      this.log(chalk2.bgYellow(chalk2.black("WATCH mode enabled!")));
    }
    this.log("");
  }
  onSpecRunning(rid) {
    this.onJobComplete(rid, this._jobs.get(rid), 0, chalk2.bold(chalk2.cyan("RUNNING")));
  }
  onSpecRetry(rid, job, retries = 0) {
    const delayMsg = this._specFileRetriesDelay > 0 ? ` after ${this._specFileRetriesDelay}s` : "";
    this.onJobComplete(rid, job, retries, chalk2.bold(chalk2.yellow("RETRYING") + delayMsg));
  }
  onSpecPass(rid, job, retries = 0) {
    this.onJobComplete(rid, job, retries, chalk2.bold(chalk2.green("PASSED")));
  }
  onSpecFailure(rid, job, retries = 0) {
    this.onJobComplete(rid, job, retries, chalk2.bold(chalk2.red("FAILED")));
  }
  onSpecSkip(rid, job) {
    this.onJobComplete(rid, job, 0, "SKIPPED", log2.info);
  }
  onJobComplete(cid, job, retries = 0, message = "", _logger = this.log) {
    const details = [`[${cid}]`, message];
    if (job) {
      details.push("in", getRunnerName(job.caps), this.getFilenames(job.specs));
    }
    if (retries > 0) {
      details.push(`(${retries} retries)`);
    }
    return _logger(...details);
  }
  onTestError(payload) {
    const error = {
      type: payload.error?.type || "Error",
      message: payload.error?.message || (typeof payload.error === "string" ? payload.error : "Unknown error."),
      stack: payload.error?.stack
    };
    return this.log(`[${payload.cid}]`, `${chalk2.red(error.type)} in "${payload.fullTitle}"
${chalk2.red(error.stack || error.message)}`);
  }
  getFilenames(specs = []) {
    if (specs.length > 0) {
      return "- " + specs.join(", ").replace(new RegExp(`${process.cwd()}`, "g"), "");
    }
    return "";
  }
  /**
   * add job to interface
   */
  addJob({ cid, caps, specs, hasTests }) {
    this._jobs.set(cid, { caps, specs, hasTests });
    if (hasTests) {
      this.onSpecRunning(cid);
    } else {
      this._skippedSpecs++;
    }
  }
  /**
   * clear job from interface
   */
  clearJob({ cid, passed, retries }) {
    const job = this._jobs.get(cid);
    this._jobs.delete(cid);
    const retryAttempts = this._specFileRetries - retries;
    const retry = !passed && retries > 0;
    if (!retry) {
      this.result.finished++;
    }
    if (job && job.hasTests === false) {
      return this.onSpecSkip(cid, job);
    }
    if (passed) {
      this.result.passed++;
      this.onSpecPass(cid, job, retryAttempts);
    } else if (retry) {
      this.totalWorkerCnt++;
      this.result.retries++;
      this.onSpecRetry(cid, job, retryAttempts);
    } else {
      this.result.failed++;
      this.onSpecFailure(cid, job, retryAttempts);
    }
  }
  /**
   * for testing purposes call console log in a static method
   */
  log(...args) {
    console.log(...args);
    return args;
  }
  logHookError(error) {
    if (error instanceof HookError) {
      return this.log(`${chalk2.red(error.name)} in "${error.origin}"
${chalk2.red(error.stack || error.message)}`);
    }
    return this.log(`${chalk2.red(error.name)}: ${chalk2.red(error.stack || error.message)}`);
  }
  /**
   * event handler that is triggered when runner sends up events
   */
  onMessage(event) {
    if (event.name === "reporterRealTime") {
      this.log(event.content);
      return;
    }
    if (event.origin === "debugger" && event.name === "start") {
      this.log(chalk2.yellow(event.params.introMessage));
      this._inDebugMode = true;
      return this._inDebugMode;
    }
    if (event.origin === "debugger" && event.name === "stop") {
      this._inDebugMode = false;
      return this._inDebugMode;
    }
    if (event.name === "testFrameworkInit") {
      return this.emit("job:start", event.content);
    }
    if (event.name === "snapshot") {
      const snapshotResults = event.content;
      return snapshotResults.forEach((snapshotResult) => {
        this.#snapshotManager.add(snapshotResult);
      });
    }
    if (event.name === "error") {
      return this.log(
        `[${event.cid}]`,
        chalk2.white(chalk2.bgRed(chalk2.bold(" Error: "))),
        event.content ? event.content.message || event.content.stack || event.content : ""
      );
    }
    if (event.origin !== "reporter" && event.origin !== "debugger") {
      if (EVENT_FILTER.includes(event.name)) {
        return;
      }
      return this.log(event.cid, event.origin, event.name, event.content);
    }
    if (event.name === "printFailureMessage") {
      return this.onTestError(event.content);
    }
    if (!this._messages[event.origin][event.name]) {
      this._messages[event.origin][event.name] = [];
    }
    this._messages[event.origin][event.name].push(event.content);
  }
  sigintTrigger() {
    if (this._inDebugMode) {
      return false;
    }
    const isRunning = this._jobs.size !== 0 || this._isWatchMode;
    const shutdownMessage = isRunning ? "Ending WebDriver sessions gracefully ...\n(press ctrl+c again to hard kill the runner)" : "Ended WebDriver sessions gracefully after a SIGINT signal was received!";
    return this.log("\n\n" + shutdownMessage);
  }
  printReporters() {
    const reporter = this._messages.reporter;
    this._messages.reporter = {};
    for (const [reporterName, messages] of Object.entries(reporter)) {
      this.log("\n", chalk2.bold(chalk2.magenta(`"${reporterName}" Reporter:`)));
      this.log(messages.join(""));
    }
  }
  printSummary() {
    const totalJobs = this.totalWorkerCnt - this.result.retries;
    const elapsed = new Date(Date.now() - this._start.getTime()).toUTCString().match(/(\d\d:\d\d:\d\d)/)[0];
    const retries = this.result.retries ? chalk2.yellow(this.result.retries, "retries") + ", " : "";
    const failed = this.result.failed ? chalk2.red(this.result.failed, "failed") + ", " : "";
    const skipped = this._skippedSpecs > 0 ? chalk2.gray(this._skippedSpecs, "skipped") + ", " : "";
    const percentCompleted = totalJobs ? Math.round(this.result.finished / totalJobs * 100) : 0;
    const snapshotSummary = this.#snapshotManager.summary;
    const snapshotNotes = [];
    if (snapshotSummary.added > 0) {
      snapshotNotes.push(chalk2.green(`${snapshotSummary.added} snapshot(s) added.`));
    }
    if (snapshotSummary.updated > 0) {
      snapshotNotes.push(chalk2.yellow(`${snapshotSummary.updated} snapshot(s) updated.`));
    }
    if (snapshotSummary.unmatched > 0) {
      snapshotNotes.push(chalk2.red(`${snapshotSummary.unmatched} snapshot(s) unmatched.`));
    }
    if (snapshotSummary.unchecked > 0) {
      snapshotNotes.push(chalk2.gray(`${snapshotSummary.unchecked} snapshot(s) unchecked.`));
    }
    if (snapshotNotes.length > 0) {
      this.log("\nSnapshot Summary:");
      snapshotNotes.forEach((note) => this.log(note));
    }
    return this.log(
      "\nSpec Files:	",
      chalk2.green(this.result.passed, "passed") + ", " + retries + failed + skipped + totalJobs,
      "total",
      `(${percentCompleted}% completed)`,
      "in",
      elapsed,
      this.#hasShard() ? `
Shard:		 ${this._config.shard.current} / ${this._config.shard.total}` : "",
      "\n"
    );
  }
  finalise() {
    this.printReporters();
    this.printSummary();
  }
};

// src/launcher.ts
var log3 = logger3("@wdio/cli:launcher");
var TS_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
var Launcher = class {
  constructor(_configFilePath, _args = {}, _isWatchMode = false) {
    this._configFilePath = _configFilePath;
    this._args = _args;
    this._isWatchMode = _isWatchMode;
    this.configParser = new ConfigParser2(this._configFilePath, this._args);
  }
  #isInitialized = false;
  configParser;
  isMultiremote = false;
  isParallelMultiremote = false;
  runner;
  interface;
  _exitCode = 0;
  _hasTriggeredExitRoutine = false;
  _schedule = [];
  _rid = [];
  _runnerStarted = 0;
  _runnerFailed = 0;
  _launcher;
  _resolve;
  /**
   * run sequence
   * @return  {Promise}  that only gets resolved with either an exitCode or an error
   */
  async run() {
    await this.initialize();
    const config = this.configParser.getConfig();
    const capabilities = this.configParser.getCapabilities();
    this.isParallelMultiremote = Array.isArray(capabilities) && capabilities.every((cap) => Object.values(cap).length > 0 && Object.values(cap).every((c) => typeof c === "object" && c.capabilities));
    this.isMultiremote = this.isParallelMultiremote || !Array.isArray(capabilities);
    validateConfig(TESTRUNNER_DEFAULTS, { ...config, capabilities });
    await enableFileLogging(config.outputDir);
    logger3.setLogLevelsConfig(config.logLevels, config.logLevel);
    const totalWorkerCnt = Array.isArray(capabilities) ? capabilities.map((c) => {
      if (this.isParallelMultiremote) {
        const keys = Object.keys(c);
        const caps2 = c[keys[0]].capabilities;
        return this.configParser.getSpecs(caps2["wdio:specs"], caps2["wdio:exclude"]).length;
      }
      const standaloneCaps = c;
      const cap = "alwaysMatch" in standaloneCaps ? standaloneCaps.alwaysMatch : standaloneCaps;
      return this.configParser.getSpecs(cap["wdio:specs"], cap["wdio:exclude"]).length;
    }).reduce((a, b) => a + b, 0) : 1;
    this.interface = new WDIOCLInterface(config, totalWorkerCnt, this._isWatchMode);
    config.runnerEnv.FORCE_COLOR = Number(this.interface.hasAnsiSupport).toString();
    const [runnerName, runnerOptions] = Array.isArray(config.runner) ? config.runner : [config.runner, {}];
    const Runner = (await initializePlugin(runnerName, "runner")).default;
    this.runner = new Runner(runnerOptions, config);
    exitHook(this._exitHandler.bind(this));
    let exitCode = 0;
    let error = void 0;
    const caps = this.configParser.getCapabilities();
    try {
      const { ignoredWorkerServices, launcherServices } = await initializeLauncherService(config, caps);
      this._launcher = launcherServices;
      this._args.ignoredWorkerServices = ignoredWorkerServices;
      await this.runner.initialize();
      log3.info("Run onPrepare hook");
      await runLauncherHook(config.onPrepare, config, caps);
      await runServiceHook(this._launcher, "onPrepare", config, caps);
      await Promise.all([
        setupDriver(config, caps),
        setupBrowser(config, caps)
      ]);
      exitCode = await this._runMode(config, caps);
      await logger3.waitForBuffer();
      this.interface.finalise();
    } catch (err) {
      error = err;
    } finally {
      if (!this._hasTriggeredExitRoutine) {
        this._hasTriggeredExitRoutine = true;
        const passesCodeCoverage = await this.runner.shutdown();
        if (!passesCodeCoverage) {
          exitCode = exitCode || 1;
        }
      }
      exitCode = await this.#runOnCompleteHook(config, caps, exitCode);
    }
    if (error) {
      this.interface.logHookError(error);
      throw error;
    }
    return exitCode;
  }
  /**
   * initialize launcher by loading `tsx` if needed
   */
  async initialize() {
    if (this.#isInitialized) {
      return;
    }
    const tsxPath = resolve2("tsx", import.meta.url);
    if (!process.env.NODE_OPTIONS || !process.env.NODE_OPTIONS.includes(tsxPath)) {
      const moduleLoaderFlag = nodeVersion("major") >= 21 || nodeVersion("major") === 20 && nodeVersion("minor") >= 6 || nodeVersion("major") === 18 && nodeVersion("minor") >= 19 ? "--import" : "--loader";
      process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS || ""} ${moduleLoaderFlag} ${tsxPath}`;
    }
    if (TS_FILE_EXTENSIONS.some((ext) => this._configFilePath.endsWith(ext))) {
      await import(tsxPath);
    }
    this.#isInitialized = true;
    await this.configParser.initialize(this._args);
  }
  /**
   * run onComplete hook
   * Even if it fails we still want to see result and end logger stream.
   * Also ensure that user hooks are run before service hooks so that e.g.
   * a user can use plugin service, e.g. shared store service is still
   * available running hooks in this order
   */
  async #runOnCompleteHook(config, caps, exitCode) {
    log3.info("Run onComplete hook");
    const onCompleteResults = await runOnCompleteHook(config.onComplete, config, caps, exitCode, this.interface.result);
    if (this._launcher) {
      await runServiceHook(this._launcher, "onComplete", exitCode, config, caps);
    }
    return onCompleteResults.includes(1) ? 1 : exitCode;
  }
  /**
   * run without triggering onPrepare/onComplete hooks
   */
  _runMode(config, caps) {
    if (
      /**
       * no caps were provided
       */
      !caps || /**
       * capability array is empty
       */
      Array.isArray(caps) && caps.length === 0 || /**
       * user wants to use multiremote but capability object is empty
       */
      !Array.isArray(caps) && Object.keys(caps).length === 0
    ) {
      return new Promise((resolve3) => {
        log3.error("Missing capabilities, exiting with failure");
        return resolve3(1);
      });
    }
    const specFileRetries = this._isWatchMode ? 0 : config.specFileRetries;
    let cid = 0;
    if (this.isMultiremote && !this.isParallelMultiremote) {
      this._schedule.push({
        cid: cid++,
        caps,
        specs: this._formatSpecs(caps, specFileRetries),
        availableInstances: config.maxInstances || 1,
        runningInstances: 0
      });
    } else {
      for (const capabilities of caps) {
        const availableInstances = this.isParallelMultiremote ? config.maxInstances || 1 : config.runner === "browser" ? 1 : capabilities["wdio:maxInstances"] || config.maxInstancesPerCapability;
        this._schedule.push({
          cid: cid++,
          caps: capabilities,
          specs: this._formatSpecs(capabilities, specFileRetries),
          availableInstances,
          runningInstances: 0
        });
      }
    }
    return new Promise((resolve3) => {
      this._resolve = resolve3;
      if (Object.values(this._schedule).reduce((specCnt, schedule) => specCnt + schedule.specs.length, 0) === 0) {
        const { total, current } = config.shard;
        if (total > 1) {
          log3.info(`No specs to execute in shard ${current}/${total}, exiting!`);
          return resolve3(0);
        }
        log3.error("No specs found to run, exiting with failure");
        return resolve3(1);
      }
      if (this._runSpecs()) {
        resolve3(0);
      }
    });
  }
  /**
   * Format the specs into an array of objects with files and retries
   */
  _formatSpecs(capabilities, specFileRetries) {
    let caps;
    if ("alwaysMatch" in capabilities) {
      caps = capabilities.alwaysMatch;
    } else if (typeof Object.keys(capabilities)[0] === "object" && "capabilities" in capabilities[Object.keys(capabilities)[0]]) {
      caps = {};
    } else {
      caps = capabilities;
    }
    const specs = (
      // @ts-expect-error deprecated
      caps.specs || caps["wdio:specs"]
    );
    const excludes = (
      // @ts-expect-error deprecated
      caps.exclude || caps["wdio:exclude"]
    );
    const files = this.configParser.getSpecs(specs, excludes);
    return files.map((file) => {
      if (typeof file === "string") {
        return { files: [file], retries: specFileRetries };
      } else if (Array.isArray(file)) {
        return { files: file, retries: specFileRetries };
      }
      log3.warn("Unexpected entry in specs that is neither string nor array: ", file);
      return { files: [], retries: specFileRetries };
    });
  }
  /**
   * run multiple single remote tests
   * @return {Boolean} true if all specs have been run and all instances have finished
   */
  _runSpecs() {
    if (this._hasTriggeredExitRoutine) {
      return true;
    }
    const config = this.configParser.getConfig();
    while (this._getNumberOfRunningInstances() < config.maxInstances) {
      const schedulableCaps = this._schedule.filter((session) => {
        const filter = typeof config.bail !== "number" || config.bail < 1 || config.bail > this._runnerFailed;
        if (!filter) {
          this._schedule.forEach((t) => {
            t.specs = [];
          });
          return false;
        }
        if (this._getNumberOfRunningInstances() >= config.maxInstances) {
          return false;
        }
        return session.availableInstances > 0 && session.specs.length > 0;
      }).sort((a, b) => a.runningInstances - b.runningInstances);
      if (schedulableCaps.length === 0) {
        break;
      }
      const specs = schedulableCaps[0].specs.shift();
      this._startInstance(
        specs.files,
        schedulableCaps[0].caps,
        schedulableCaps[0].cid,
        specs.rid,
        specs.retries
      );
      schedulableCaps[0].availableInstances--;
      schedulableCaps[0].runningInstances++;
    }
    return this._getNumberOfRunningInstances() === 0 && this._getNumberOfSpecsLeft() === 0;
  }
  /**
   * gets number of all running instances
   * @return {number} number of running instances
   */
  _getNumberOfRunningInstances() {
    return this._schedule.map((a) => a.runningInstances).reduce((a, b) => a + b);
  }
  /**
   * get number of total specs left to complete whole suites
   * @return {number} specs left to complete suite
   */
  _getNumberOfSpecsLeft() {
    return this._schedule.map((a) => a.specs.length).reduce((a, b) => a + b);
  }
  /**
   * Start instance in a child process.
   * @param  {Array} specs  Specs to run
   * @param  {number} cid  Capabilities ID
   * @param  {string} rid  Runner ID override
   * @param  {number} retries  Number of retries remaining
   */
  async _startInstance(specs, caps, cid, rid, retries) {
    if (!this.runner || !this.interface) {
      throw new Error("Internal Error: no runner initialized, call run() first");
    }
    const config = this.configParser.getConfig();
    if (typeof config.specFileRetriesDelay === "number" && config.specFileRetries > 0 && config.specFileRetries !== retries) {
      await sleep(config.specFileRetriesDelay * 1e3);
    }
    const runnerId = rid || this._getRunnerId(cid);
    const processNumber = this._runnerStarted + 1;
    const debugArgs = [];
    let debugType;
    let debugHost = "";
    const debugPort = process.debugPort;
    for (const arg of process.execArgv) {
      const debugArgs2 = arg.match("--(debug|inspect)(?:-brk)?(?:=(.*):)?");
      if (debugArgs2) {
        const [, type, host] = debugArgs2;
        if (type) {
          debugType = type;
        }
        if (host) {
          debugHost = `${host}:`;
        }
      }
    }
    if (debugType) {
      debugArgs.push(`--${debugType}=${debugHost}${debugPort + processNumber}`);
    }
    const capExecArgs = [...config.execArgv || []];
    const defaultArgs = capExecArgs.length ? process.execArgv : [];
    const execArgv = [...defaultArgs, ...debugArgs, ...capExecArgs];
    this._runnerStarted++;
    const workerCaps = structuredClone(caps);
    log3.info("Run onWorkerStart hook");
    await runLauncherHook(config.onWorkerStart, runnerId, workerCaps, specs, this._args, execArgv).catch((error) => this._workerHookError(error));
    await runServiceHook(this._launcher, "onWorkerStart", runnerId, workerCaps, specs, this._args, execArgv).catch((error) => this._workerHookError(error));
    const worker = await this.runner.run({
      cid: runnerId,
      command: "run",
      configFile: this._configFilePath,
      args: {
        ...this._args,
        /**
         * Pass on user and key values to ensure they are available in the worker process when using
         * environment variables that were locally exported but not part of the environment.
         */
        user: config.user,
        key: config.key
      },
      caps: workerCaps,
      specs,
      execArgv,
      retries
    });
    worker.on("message", this.interface.onMessage.bind(this.interface));
    worker.on("error", this.interface.onMessage.bind(this.interface));
    worker.on("exit", (code) => {
      if (!this.configParser.getConfig().groupLogsByTestSpec) {
        return;
      }
      if (code.exitCode === 0) {
        console.log(WORKER_GROUPLOGS_MESSAGES.normalExit(code.cid));
      } else {
        console.log(WORKER_GROUPLOGS_MESSAGES.exitWithError(code.cid));
      }
      worker.logsAggregator.forEach((logLine) => {
        console.log(logLine.replace(new RegExp("\\n$"), ""));
      });
    });
    worker.on("exit", this._endHandler.bind(this));
  }
  _workerHookError(error) {
    if (!this.interface) {
      throw new Error("Internal Error: no interface initialized, call run() first");
    }
    this.interface.logHookError(error);
    if (this._resolve) {
      this._resolve(1);
    }
  }
  /**
   * generates a runner id
   * @param  {number} cid capability id (unique identifier for a capability)
   * @return {String}     runner id (combination of cid and test id e.g. 0a, 0b, 1a, 1b ...)
   */
  _getRunnerId(cid) {
    if (!this._rid[cid]) {
      this._rid[cid] = 0;
    }
    return `${cid}-${this._rid[cid]++}`;
  }
  /**
   * Close test runner process once all child processes have exited
   * @param  {number} cid       Capabilities ID
   * @param  {number} exitCode  exit code of child process
   * @param  {Array} specs      Specs that were run
   * @param  {number} retries   Number or retries remaining
   */
  async _endHandler({ cid: rid, exitCode, specs, retries }) {
    const passed = this._isWatchModeHalted() || exitCode === 0;
    if (!passed && retries > 0) {
      const requeue = this.configParser.getConfig().specFileRetriesDeferred ? "push" : "unshift";
      this._schedule[parseInt(rid, 10)].specs[requeue]({ files: specs, retries: retries - 1, rid });
    } else {
      this._exitCode = this._isWatchModeHalted() ? 0 : this._exitCode || exitCode;
      this._runnerFailed += !passed ? 1 : 0;
    }
    if (!this._isWatchModeHalted() && this.interface) {
      this.interface.emit("job:end", { cid: rid, passed, retries });
    }
    const cid = parseInt(rid, 10);
    this._schedule[cid].availableInstances++;
    this._schedule[cid].runningInstances--;
    log3.info("Run onWorkerEnd hook");
    const config = this.configParser.getConfig();
    await runLauncherHook(config.onWorkerEnd, rid, exitCode, specs, retries).catch((error) => this._workerHookError(error));
    await runServiceHook(this._launcher, "onWorkerEnd", rid, exitCode, specs, retries).catch((error) => this._workerHookError(error));
    const shouldRunSpecs = this._runSpecs();
    const inWatchMode = this._isWatchMode && !this._hasTriggeredExitRoutine;
    if (!shouldRunSpecs || inWatchMode) {
      if (inWatchMode) {
        this.interface?.finalise();
      }
      return;
    }
    if (this._resolve) {
      this._resolve(passed ? this._exitCode : 1);
    }
  }
  /**
   * We need exitHandler to catch SIGINT / SIGTERM events.
   * Make sure all started selenium sessions get closed properly and prevent
   * having dead driver processes. To do so let the runner end its Selenium
   * session first before killing
   */
  _exitHandler(callback) {
    if (!callback || !this.runner || !this.interface) {
      return;
    }
    if (this._hasTriggeredExitRoutine) {
      return callback(true);
    }
    this._hasTriggeredExitRoutine = true;
    this.interface.sigintTrigger();
    return this.runner.shutdown().then(callback);
  }
  /**
   * returns true if user stopped watch mode, ex with ctrl+c
   * @returns {boolean}
   */
  _isWatchModeHalted() {
    return this._isWatchMode && this._hasTriggeredExitRoutine;
  }
};
var launcher_default = Launcher;

// src/run.ts
import path7 from "node:path";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";

// src/commands/config.ts
var config_exports = {};
__export(config_exports, {
  builder: () => builder,
  canAccessConfigPath: () => canAccessConfigPath,
  cmdArgs: () => cmdArgs,
  command: () => command,
  desc: () => desc,
  formatConfigFilePaths: () => formatConfigFilePaths,
  handler: () => handler,
  missingConfigurationPrompt: () => missingConfigurationPrompt,
  parseAnswers: () => parseAnswers,
  runConfigCommand: () => runConfigCommand
});
import fs3 from "node:fs/promises";
import fss from "node:fs";
import path3 from "node:path";
import inquirer2 from "inquirer";
var hasYarnLock = false;
try {
  fss.accessSync("yarn.lock");
  hasYarnLock = true;
} catch {
  hasYarnLock = false;
}
var command = "config";
var desc = "Initialize WebdriverIO and setup configuration in your current project.";
var cmdArgs = {
  yarn: {
    type: "boolean",
    desc: "Install packages via Yarn package manager.",
    default: hasYarnLock
  },
  yes: {
    alias: "y",
    desc: "will fill in all config defaults without prompting",
    type: "boolean",
    default: false
  },
  npmTag: {
    alias: "t",
    desc: "define NPM tag to use for WebdriverIO related packages",
    type: "string",
    default: "latest"
  }
};
var builder = (yargs2) => {
  return yargs2.options(cmdArgs).epilogue(CLI_EPILOGUE).help();
};
var parseAnswers = async function(yes) {
  console.log(CONFIG_HELPER_INTRO);
  const answers = await getAnswers(yes);
  const frameworkPackage = convertPackageHashToObject(answers.framework);
  const runnerPackage = convertPackageHashToObject(answers.runner || SUPPORTED_PACKAGES.runner[0].value);
  const servicePackages = answers.services.map((service) => convertPackageHashToObject(service));
  const pluginPackages = answers.plugins.map((plugin) => convertPackageHashToObject(plugin));
  const serenityPackages = getSerenityPackages(answers);
  const reporterPackages = answers.reporters.map((reporter) => convertPackageHashToObject(reporter));
  const presetPackage = convertPackageHashToObject(answers.preset || "");
  const projectProps = await getProjectProps(process.cwd());
  const projectRootDir = await getProjectRoot(answers);
  const packagesToInstall = [
    runnerPackage.package,
    frameworkPackage.package,
    presetPackage.package,
    ...reporterPackages.map((reporter) => reporter.package),
    ...pluginPackages.map((plugin) => plugin.package),
    ...servicePackages.map((service) => service.package),
    ...serenityPackages
  ].filter(Boolean);
  const hasRootTSConfig = await fs3.access(path3.resolve(projectRootDir, "tsconfig.json")).then(() => true, () => false);
  const tsConfigFilePath = !hasRootTSConfig ? path3.resolve(projectRootDir, "tsconfig.json") : answers.specs ? path3.resolve(
    path3.dirname(answers.specs.split(path3.sep).filter((s) => !s.includes("*")).join(path3.sep)),
    "tsconfig.json"
  ) : path3.resolve(projectRootDir, `tsconfig.${runnerPackage.short === "local" ? "e2e" : "wdio"}.json`);
  const parsedPaths = getPathForFileGeneration(answers, projectRootDir);
  const isUsingTypeScript = answers.isUsingTypeScript;
  const wdioConfigFilename = `wdio.conf.${isUsingTypeScript ? "ts" : "js"}`;
  const wdioConfigPath = path3.resolve(projectRootDir, wdioConfigFilename);
  return {
    projectName: projectProps?.packageJson.name || "Test Suite",
    // default values required in templates
    ...{
      usePageObjects: false,
      installTestingLibrary: false
    },
    ...answers,
    useSauceConnect: isNuxtProject || answers.useSauceConnect,
    rawAnswers: answers,
    runner: runnerPackage.short,
    preset: presetPackage.short,
    framework: frameworkPackage.short,
    purpose: runnerPackage.purpose,
    serenityAdapter: frameworkPackage.package === "@serenity-js/webdriverio" && frameworkPackage.purpose,
    reporters: reporterPackages.map(({ short }) => short),
    plugins: pluginPackages.map(({ short }) => short),
    services: servicePackages.map(({ short }) => short),
    specs: answers.specs && `./${path3.relative(projectRootDir, answers.specs).replaceAll(path3.sep, "/")}`,
    stepDefinitions: answers.stepDefinitions && `./${path3.relative(projectRootDir, answers.stepDefinitions).replaceAll(path3.sep, "/")}`,
    packagesToInstall,
    isUsingTypeScript,
    esmSupport: projectProps && !projectProps.esmSupported ? false : true,
    isSync: false,
    _async: "async ",
    _await: "await ",
    projectRootDir,
    destSpecRootPath: parsedPaths.destSpecRootPath,
    destStepRootPath: parsedPaths.destStepRootPath,
    destPageObjectRootPath: parsedPaths.destPageObjectRootPath,
    destSerenityLibRootPath: parsedPaths.destSerenityLibRootPath,
    relativePath: parsedPaths.relativePath,
    hasRootTSConfig,
    tsConfigFilePath,
    tsProject: `./${path3.relative(projectRootDir, tsConfigFilePath).replaceAll(path3.sep, "/")}`,
    wdioConfigPath
  };
};
async function runConfigCommand(parsedAnswers, npmTag) {
  console.log("\n");
  await createPackageJSON(parsedAnswers);
  await setupTypeScript(parsedAnswers);
  await npmInstall(parsedAnswers, npmTag);
  await createWDIOConfig(parsedAnswers);
  await createWDIOScript(parsedAnswers);
  console.log(
    configHelperSuccessMessage({
      projectRootDir: parsedAnswers.projectRootDir,
      runScript: parsedAnswers.serenityAdapter ? "serenity" : "wdio",
      extraInfo: parsedAnswers.serenityAdapter ? CONFIG_HELPER_SERENITY_BANNER : ""
    })
  );
  await runAppiumInstaller(parsedAnswers);
}
async function handler(argv, runConfigCmd = runConfigCommand) {
  const parsedAnswers = await parseAnswers(argv.yes);
  await runConfigCmd(parsedAnswers, argv.npmTag);
  return {
    success: true,
    parsedAnswers,
    installedPackages: parsedAnswers.packagesToInstall.map((pkg2) => pkg2.split("--")[0])
  };
}
async function formatConfigFilePaths(config) {
  const fullPath = path3.isAbsolute(config) ? config : path3.join(process.cwd(), config);
  const fullPathNoExtension = fullPath.substring(0, fullPath.lastIndexOf(path3.extname(fullPath)));
  return { fullPath, fullPathNoExtension };
}
async function canAccessConfigPath(configPathNoExtension, configPath) {
  return new Promise((resolve3, reject) => {
    if (configPath) {
      fs3.access(configPath).then(() => resolve3(configPath), reject);
    } else {
      reject();
    }
  }).catch(() => Promise.all(SUPPORTED_CONFIG_FILE_EXTENSION.map(async (supportedExtension) => {
    const configPathWithExtension = `${configPathNoExtension}.${supportedExtension}`;
    return fs3.access(configPathWithExtension).then(() => configPathWithExtension, () => void 0);
  })).then(
    (configFilePaths) => configFilePaths.find(Boolean),
    () => void 0
  ));
}
async function missingConfigurationPrompt(command5, configPath, runConfigCmd = runConfigCommand) {
  const message = `Could not execute "${command5}" due to missing configuration, file "${path3.parse(configPath).name}[.js/.ts]" not found! Would you like to create one?`;
  const { config } = await inquirer2.prompt({
    type: "confirm",
    name: "config",
    message,
    default: false
  });
  if (!config) {
    console.log(`No WebdriverIO configuration found in "${process.cwd()}"`);
    if (!process.env.WDIO_UNIT_TESTS) {
      process.exit(0);
    }
    return configPath;
  }
  const parsedAnswers = await parseAnswers(false);
  await runConfigCmd(parsedAnswers, "latest");
  return configPath;
}

// src/commands/install.ts
var install_exports = {};
__export(install_exports, {
  builder: () => builder2,
  cmdArgs: () => cmdArgs2,
  command: () => command2,
  desc: () => desc2,
  handler: () => handler2
});
import fs4 from "node:fs/promises";
import path4 from "node:path";
var supportedInstallations = {
  runner: SUPPORTED_PACKAGES.runner.map(({ value }) => convertPackageHashToObject(value)),
  plugin: SUPPORTED_PACKAGES.plugin.map(({ value }) => convertPackageHashToObject(value)),
  service: SUPPORTED_PACKAGES.service.map(({ value }) => convertPackageHashToObject(value)),
  reporter: SUPPORTED_PACKAGES.reporter.map(({ value }) => convertPackageHashToObject(value)),
  framework: SUPPORTED_PACKAGES.framework.map(({ value }) => convertPackageHashToObject(value))
};
var command2 = "install <type> <name>";
var desc2 = [
  "Add a `reporter`, `service`, or `framework` to your WebdriverIO project.",
  "The command installs the package from NPM, adds it to your package.json",
  "and modifies the wdio.conf.js accordingly."
].join(" ");
var cmdArgs2 = {
  config: {
    desc: "Location of your WDIO configuration (default: wdio.conf.(js|ts|cjs|mjs))"
  }
};
var builder2 = (yargs2) => {
  yargs2.options(cmdArgs2).epilogue(CLI_EPILOGUE).help();
  for (const [type, plugins] of Object.entries(supportedInstallations)) {
    for (const plugin of plugins) {
      yargs2.example(`$0 install ${type} ${plugin.short}`, `Install ${plugin.package}`);
    }
  }
  return yargs2;
};
async function handler2(argv) {
  const { type, name, config } = argv;
  if (!Object.keys(supportedInstallations).includes(type)) {
    console.log(`Type ${type} is not supported.`);
    process.exit(0);
    return;
  }
  const options = supportedInstallations[type].map((pkg2) => pkg2.short);
  if (!options.find((pkg2) => pkg2 === name)) {
    console.log(
      `Error: ${name} is not a supported ${type}.

Available options for a ${type} are:
- ${options.join("\n- ")}`
    );
    process.exit(0);
    return;
  }
  const defaultPath = path4.resolve(process.cwd(), "wdio.conf");
  const wdioConfPathWithNoExtension = config ? (await formatConfigFilePaths(config)).fullPathNoExtension : defaultPath;
  const wdioConfPath = await canAccessConfigPath(wdioConfPathWithNoExtension);
  if (!wdioConfPath) {
    try {
      await missingConfigurationPrompt("install", wdioConfPathWithNoExtension);
      return handler2(argv);
    } catch {
      process.exit(1);
      return;
    }
  }
  const configFile = await fs4.readFile(wdioConfPath, { encoding: "utf-8" });
  const match = findInConfig(configFile, type);
  const projectRoot = await getProjectRoot();
  if (match && match[0].includes(name)) {
    console.log(`The ${type} ${name} is already part of your configuration.`);
    process.exit(0);
    return;
  }
  const selectedPackage = supportedInstallations[type].find(({ short }) => short === name);
  const pkgsToInstall = selectedPackage ? [selectedPackage.package] : [];
  addServiceDeps(selectedPackage ? [selectedPackage] : [], pkgsToInstall, true);
  const pm = detectPackageManager();
  console.log(`Installing "${selectedPackage.package}" using ${pm}.`);
  const success = await installPackages(projectRoot, pkgsToInstall, true);
  if (!success) {
    process.exit(1);
    return;
  }
  console.log(`Package "${selectedPackage.package}" installed successfully.`);
  const newConfig = replaceConfig(configFile, type, name);
  if (!newConfig) {
    throw new Error(`Couldn't find "${type}" property in ${path4.basename(wdioConfPath)}`);
  }
  await fs4.writeFile(wdioConfPath, newConfig, { encoding: "utf-8" });
  console.log("Your wdio.conf.js file has been updated.");
  process.exit(0);
}

// src/commands/repl.ts
var repl_exports = {};
__export(repl_exports, {
  builder: () => builder4,
  cmdArgs: () => cmdArgs4,
  command: () => command4,
  desc: () => desc4,
  handler: () => handler4
});
import pickBy3 from "lodash.pickby";
import { remote } from "webdriverio";

// src/commands/run.ts
var run_exports = {};
__export(run_exports, {
  builder: () => builder3,
  cmdArgs: () => cmdArgs3,
  command: () => command3,
  desc: () => desc3,
  handler: () => handler3,
  launch: () => launch,
  launchWithStdin: () => launchWithStdin
});
import fs5 from "node:fs/promises";
import path6 from "node:path";

// src/watcher.ts
import url from "node:url";
import path5 from "node:path";
import chokidar from "chokidar";
import pickBy2 from "lodash.pickby";
import flattenDeep from "lodash.flattendeep";
import union from "lodash.union";
import logger4 from "@wdio/logger";
import { FileSystemPathService } from "@wdio/config/node";
var log4 = logger4("@wdio/cli:watch");
var Watcher = class {
  constructor(_configFile, _args) {
    this._configFile = _configFile;
    this._args = _args;
    log4.info("Starting launcher in watch mode");
    this._launcher = new launcher_default(this._configFile, this._args, true);
  }
  _launcher;
  _specs = [];
  async watch() {
    await this._launcher.initialize();
    const specs = this._launcher.configParser.getSpecs();
    const capSpecs = this._launcher.isMultiremote ? [] : union(flattenDeep(
      this._launcher.configParser.getCapabilities().map((cap) => "alwaysMatch" in cap ? cap.alwaysMatch["wdio:specs"] : cap["wdio:specs"] || [])
    ));
    this._specs = [...specs, ...capSpecs];
    const flattenedSpecs = flattenDeep(this._specs).map((fileUrl) => url.fileURLToPath(fileUrl));
    chokidar.watch(flattenedSpecs, { ignoreInitial: true }).on("add", this.getFileListener()).on("change", this.getFileListener());
    const { filesToWatch } = this._launcher.configParser.getConfig();
    if (filesToWatch.length) {
      const pathService = new FileSystemPathService();
      const rootDir = path5.dirname(path5.resolve(process.cwd(), this._configFile));
      const globbedFilesToWatch = filesToWatch.map((file) => pathService.ensureAbsolutePath(file, rootDir));
      chokidar.watch(globbedFilesToWatch, { ignoreInitial: true }).on("add", this.getFileListener(false)).on("change", this.getFileListener(false));
    }
    await this._launcher.run();
    const workers = this.getWorkers();
    Object.values(workers).forEach((worker) => worker.on("exit", () => {
      if (Object.values(workers).find((w) => w.isBusy)) {
        return;
      }
      this._launcher.interface?.finalise();
    }));
  }
  /**
   * return file listener callback that calls `run` method
   * @param  {Boolean}  [passOnFile=true]  if true pass on file change as parameter
   * @return {Function}                    chokidar event callback
   */
  getFileListener(passOnFile = true) {
    return (spec) => {
      const runSpecs = [];
      let singleSpecFound = false;
      for (let index = 0, length = this._specs.length; index < length; index += 1) {
        const value = this._specs[index];
        if (Array.isArray(value) && value.indexOf(spec) > -1) {
          runSpecs.push(value);
        } else if (!singleSpecFound && spec === value) {
          singleSpecFound = true;
          runSpecs.push(value);
        }
      }
      if (runSpecs.length === 0) {
        runSpecs.push(url.pathToFileURL(spec).href);
      }
      const { spec: _, ...args } = this._args;
      return runSpecs.map((spec2) => {
        return this.run({
          ...args,
          ...passOnFile ? { spec: [spec2] } : {}
        });
      });
    };
  }
  /**
   * helper method to get workers from worker pool of wdio runner
   * @param  predicate         filter by property value (see lodash.pickBy)
   * @param  includeBusyWorker don't filter out busy worker (default: false)
   * @return                   Object with workers, e.g. {'0-0': { ... }}
   */
  getWorkers(predicate, includeBusyWorker) {
    if (!this._launcher.runner) {
      throw new Error("Internal Error: no runner initialized, call run() first");
    }
    let workers = this._launcher.runner.workerPool;
    if (typeof predicate === "function") {
      workers = pickBy2(workers, predicate);
    }
    if (!includeBusyWorker) {
      workers = pickBy2(workers, (worker) => !worker.isBusy);
    }
    return workers;
  }
  /**
   * run workers with params
   * @param  params parameters to run the worker with
   */
  run(params = {}) {
    const workers = this.getWorkers(
      params.spec ? (worker) => Boolean(worker.specs.find((s) => params.spec?.includes(s))) : void 0
    );
    if (Object.keys(workers).length === 0 || !this._launcher.interface) {
      return;
    }
    this._launcher.interface.totalWorkerCnt = Object.entries(workers).length;
    this.cleanUp();
    for (const [, worker] of Object.entries(workers)) {
      const { cid, capabilities, specs, sessionId } = worker;
      const { hostname, path: path8, port, protocol, automationProtocol } = worker.config;
      const args = Object.assign({ sessionId, baseUrl: worker.config.baseUrl, hostname, path: path8, port, protocol, automationProtocol }, params);
      worker.postMessage("run", args);
      this._launcher.interface.emit("job:start", { cid, caps: capabilities, specs });
    }
  }
  cleanUp() {
    this._launcher.interface?.setup();
  }
};

// src/commands/run.ts
var command3 = "run <configPath>";
var desc3 = "Run your WDIO configuration file to initialize your tests. (default)";
var cmdArgs3 = {
  watch: {
    desc: "Run WebdriverIO in watch mode",
    type: "boolean"
  },
  hostname: {
    alias: "h",
    desc: "automation driver host address",
    type: "string"
  },
  port: {
    alias: "p",
    desc: "automation driver port",
    type: "number"
  },
  path: {
    type: "string",
    desc: 'path to WebDriver endpoints (default "/")'
  },
  user: {
    alias: "u",
    desc: "username if using a cloud service as automation backend",
    type: "string"
  },
  key: {
    alias: "k",
    desc: "corresponding access key to the user",
    type: "string"
  },
  logLevel: {
    alias: "l",
    desc: "level of logging verbosity",
    choices: ["trace", "debug", "info", "warn", "error", "silent"]
  },
  bail: {
    desc: "stop test runner after specific amount of tests have failed",
    type: "number"
  },
  baseUrl: {
    desc: "shorten url command calls by setting a base url",
    type: "string"
  },
  waitforTimeout: {
    alias: "w",
    desc: "timeout for all waitForXXX commands",
    type: "number"
  },
  updateSnapshots: {
    alias: "s",
    desc: "update DOM, image or test snapshots",
    type: "string",
    coerce: (value) => {
      if (value === "") {
        return "all";
      }
      return value;
    }
  },
  framework: {
    alias: "f",
    desc: "defines the framework (Mocha, Jasmine or Cucumber) to run the specs",
    type: "string"
  },
  reporters: {
    alias: "r",
    desc: "reporters to print out the results on stdout",
    type: "array"
  },
  suite: {
    desc: "overwrites the specs attribute and runs the defined suite",
    type: "array"
  },
  spec: {
    desc: "run only a certain spec file or wildcard - overrides specs piped from stdin",
    type: "array"
  },
  exclude: {
    desc: "exclude certain spec file or wildcard from the test run - overrides exclude piped from stdin",
    type: "array"
  },
  "repeat": {
    desc: "Repeat specific specs and/or suites N times",
    type: "number"
  },
  mochaOpts: {
    desc: "Mocha options",
    coerce: coerceOptsFor("mocha")
  },
  jasmineOpts: {
    desc: "Jasmine options",
    coerce: coerceOptsFor("jasmine")
  },
  cucumberOpts: {
    desc: "Cucumber options",
    coerce: coerceOptsFor("cucumber")
  },
  coverage: {
    desc: "Enable coverage for browser runner"
  },
  shard: {
    desc: "Shard tests and execute only the selected shard. Specify in the one-based form like `--shard x/y`, where x is the current and y the total shard.",
    coerce: (shard) => {
      const [current, total] = shard.split("/").map(Number);
      if (Number.isNaN(current) || Number.isNaN(total)) {
        throw new Error("Shard parameter must be in the form `x/y`, where x and y are positive integers.");
      }
      return { current, total };
    }
  }
};
var builder3 = (yargs2) => {
  return yargs2.options(cmdArgs3).example("$0 run wdio.conf.js --suite foobar", 'Run suite on testsuite "foobar"').example("$0 run wdio.conf.js --spec ./tests/e2e/a.js --spec ./tests/e2e/b.js", "Run suite on specific specs").example("$0 run wdio.conf.js --shard 1/4", "Run only the first shard of 4 shards").example("$0 run wdio.conf.js --mochaOpts.timeout 60000", "Run suite with custom Mocha timeout").example("$0 run wdio.conf.js --tsConfigPath=./configs/bdd-tsconfig.json", "Run suite with tsx using custom tsconfig.json").epilogue(CLI_EPILOGUE).help();
};
function launchWithStdin(wdioConfPath, params) {
  let stdinData = "";
  process.stdin.resume();
  const stdin = process.stdin;
  stdin.setEncoding("utf8");
  stdin.on("data", (data) => {
    stdinData += data;
  });
  stdin.on("end", () => {
    if (stdinData.length > 0) {
      params.spec = stdinData.trim().split(/\r?\n/);
    }
    launch(wdioConfPath, params);
  });
}
async function launch(wdioConfPath, params) {
  const launcher = new launcher_default(wdioConfPath, params);
  return launcher.run().then((...args) => {
    if (!process.env.WDIO_UNIT_TESTS) {
      process.exit(...args);
    }
  }).catch((err) => {
    console.error(err);
    if (!process.env.WDIO_UNIT_TESTS) {
      process.exit(1);
    }
  });
}
async function handler3(argv) {
  const { configPath = "wdio.conf.js", ...params } = argv;
  const wdioConf = await formatConfigFilePaths(configPath);
  const confAccess = await canAccessConfigPath(wdioConf.fullPathNoExtension, wdioConf.fullPath);
  if (!confAccess) {
    try {
      await missingConfigurationPrompt("run", wdioConf.fullPathNoExtension);
      if (process.env.WDIO_UNIT_TESTS) {
        return;
      }
      return handler3(argv);
    } catch {
      process.exit(1);
    }
  }
  const tsConfigPathFromEnvVar = process.env.TSCONFIG_PATH && path6.resolve(process.cwd(), process.env.TSCONFIG_PATH) || process.env.TSX_TSCONFIG_PATH && path6.resolve(process.cwd(), process.env.TSX_TSCONFIG_PATH);
  const tsConfigPathFromParams = params.tsConfigPath && path6.resolve(process.cwd(), params.tsConfigPath);
  const tsConfigPathRelativeToWdioConfig = path6.join(path6.dirname(confAccess), "tsconfig.json");
  const localTSConfigPath = tsConfigPathFromEnvVar || tsConfigPathFromParams || tsConfigPathRelativeToWdioConfig;
  const hasLocalTSConfig = await fs5.access(localTSConfigPath).then(() => true, () => false);
  if (hasLocalTSConfig) {
    process.env.TSX_TSCONFIG_PATH = localTSConfigPath;
  }
  if (params.watch) {
    const watcher = new Watcher(confAccess, params);
    return watcher.watch();
  }
  if (process.stdin.isTTY || !process.stdout.isTTY) {
    return launch(confAccess, params);
  }
  launchWithStdin(confAccess, params);
}

// src/commands/repl.ts
var IGNORED_ARGS = [
  "bail",
  "framework",
  "reporters",
  "suite",
  "spec",
  "exclude",
  "mochaOpts",
  "jasmineOpts",
  "cucumberOpts"
];
var command4 = "repl <option> [capabilities]";
var desc4 = "Run WebDriver session in command line";
var cmdArgs4 = {
  platformVersion: {
    alias: "v",
    desc: "Version of OS for mobile devices",
    type: "string"
  },
  deviceName: {
    alias: "d",
    desc: "Device name for mobile devices",
    type: "string"
  },
  udid: {
    alias: "u",
    desc: "UDID of real mobile devices",
    type: "string"
  }
};
var builder4 = (yargs2) => {
  return yargs2.options(pickBy3({ ...cmdArgs4, ...cmdArgs3 }, (_, key) => !IGNORED_ARGS.includes(key))).example("$0 repl firefox --path /", "Run repl locally").example("$0 repl chrome -u <SAUCE_USERNAME> -k <SAUCE_ACCESS_KEY>", "Run repl in Sauce Labs cloud").example("$0 repl android", "Run repl browser on launched Android device").example('$0 repl "./path/to/your_app.app"', "Run repl native app on iOS simulator").example('$0 repl ios -v 11.3 -d "iPhone 7" -u 123432abc', "Run repl browser on iOS device with capabilities").example('$0 repl "./path/to/wdio.config.js" 0 -p 9515', "Run repl using the first capability from the capabilty array in wdio.config.js").example('$0 repl "./path/to/wdio.config.js" "myChromeBrowser" -p 9515', "Run repl using a named multiremote capabilities in wdio.config.js").epilogue(CLI_EPILOGUE).help();
};
var handler4 = async (argv) => {
  const caps = await getCapabilities(argv);
  const client = await remote({ ...argv, ...caps });
  global.$ = client.$.bind(client);
  global.$$ = client.$$.bind(client);
  global.browser = client;
  await client.debug();
  return client.deleteSession();
};

// src/commands/index.ts
var commands = [config_exports, install_exports, repl_exports, run_exports];

// src/run.ts
var DEFAULT_CONFIG_FILENAME = "wdio.conf.js";
var DESCRIPTION = [
  "The `wdio` command allows you run and manage your WebdriverIO test suite.",
  "If no command is provided it calls the `run` command by default, so:",
  "",
  "$ wdio wdio.conf.js",
  "",
  "is the same as:",
  "$ wdio run wdio.conf.js",
  "",
  "For more information, visit: https://webdriver.io/docs/clioptions"
];
async function run() {
  const argv = yargs(hideBin(process.argv)).command(commands).example("wdio run wdio.conf.js --suite foobar", 'Run suite on testsuite "foobar"').example("wdio run wdio.conf.js --spec ./tests/e2e/a.js --spec ./tests/e2e/b.js", "Run suite on specific specs").example("wdio run wdio.conf.js --spec ./tests/e2e/a.feature:5", "Run scenario by line number").example("wdio run wdio.conf.js --spec ./tests/e2e/a.feature:5:10", "Run scenarios by line number").example("wdio run wdio.conf.js --spec ./tests/e2e/a.feature:5:10 --spec ./test/e2e/b.feature", "Run scenarios by line number in single feature and another complete feature").example("wdio install reporter spec", "Install @wdio/spec-reporter").example("wdio repl chrome -u <SAUCE_USERNAME> -k <SAUCE_ACCESS_KEY>", "Run repl in Sauce Labs cloud").updateStrings({ "Commands:": `${DESCRIPTION.join("\n")}

Commands:` }).version(pkg.version).epilogue(CLI_EPILOGUE);
  if (!process.argv.find((arg) => arg === "--help")) {
    argv.options(cmdArgs3);
  }
  const params = await argv.parse();
  if (!params._ || params._.find((param) => SUPPORTED_COMMANDS.includes(param))) {
    return;
  }
  const args = {
    ...params,
    configPath: path7.resolve(process.cwd(), params._[0] && params._[0].toString() || DEFAULT_CONFIG_FILENAME)
  };
  try {
    const cp = await handler3(args);
    return cp;
  } catch (err) {
    const output = await new Promise(
      (resolve3) => yargs(hideBin(process.argv)).parse("--help", (err2, argv2, output2) => resolve3(output2))
    );
    console.error(`${output}

${err.stack}`);
    if (!process.env.WDIO_UNIT_TESTS) {
      process.exit(1);
    }
  }
}
export {
  launcher_default as Launcher,
  run
};