'use strict';

var xvfb = require('./xvfb-D-nbLwPu.js');
var _ = require('lodash');
var commander = require('commander');
var commonTags = require('common-tags');
var logSymbols = require('log-symbols');
var Debug = require('debug');
var fs = require('fs-extra');
var path = require('path');
var Table = require('cli-table3');
var dayjs = require('dayjs');
var relativeTime = require('dayjs/plugin/relativeTime');
var chalk = require('chalk');
var Bluebird = require('bluebird');
var spawn = require('./spawn-Cbp8Y4I3.js');
var os = require('os');
var listr2 = require('listr2');
var timers = require('timers/promises');
var promises = require('fs/promises');
var assert = require('assert');
var request = require('@cypress/request');
var requestProgress = require('request-progress');
var proxyFromEnv = require('proxy-from-env');
var cp = require('child_process');
var yauzl = require('yauzl');
var extract = require('extract-zip');
var readline = require('readline');
var prettyBytes = require('pretty-bytes');

const debug$6 = Debug('cypress:cli');
const defaultBaseUrl = 'https://download.cypress.io/';
const defaultMaxRedirects = 10;
const getProxyForUrlWithNpmConfig = (url) => {
    return proxyFromEnv.getProxyForUrl(url) ||
        process.env.npm_config_https_proxy ||
        process.env.npm_config_proxy ||
        null;
};
const getBaseUrl = () => {
    if (xvfb.util.getEnv('CYPRESS_DOWNLOAD_MIRROR')) {
        let baseUrl = xvfb.util.getEnv('CYPRESS_DOWNLOAD_MIRROR');
        if (!(baseUrl === null || baseUrl === void 0 ? void 0 : baseUrl.endsWith('/'))) {
            baseUrl += '/';
        }
        return baseUrl || defaultBaseUrl;
    }
    return defaultBaseUrl;
};
const getCA = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
    if (process.env.npm_config_cafile) {
        try {
            const caFileContent = yield fs.readFile(process.env.npm_config_cafile, 'utf8');
            return caFileContent;
        }
        catch (error) {
            debug$6('error reading ca file', error);
            return;
        }
    }
    if (process.env.npm_config_ca) {
        return process.env.npm_config_ca;
    }
    return;
});
const prepend = (arch, urlPath, version) => {
    const endpoint = new URL(urlPath, getBaseUrl()).toString();
    const platform = os.platform();
    const pathTemplate = xvfb.util.getEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', true);
    if ((platform === 'win32') && (arch === 'arm64')) {
        debug$6(`detected platform ${platform} architecture ${arch} combination`);
        arch = 'x64';
        debug$6(`overriding to download ${platform}-${arch} instead`);
    }
    return pathTemplate
        ? (pathTemplate
            .replace(/\\?\$\{endpoint\}/g, endpoint)
            .replace(/\\?\$\{platform\}/g, platform)
            .replace(/\\?\$\{arch\}/g, arch)
            .replace(/\\?\$\{version\}/g, version))
        : `${endpoint}?platform=${platform}&arch=${arch}`;
};
const getUrl = (arch, version) => {
    if (_.isString(version) && version.match(/^https?:\/\/.*$/)) {
        debug$6('version is already an url', version);
        return version;
    }
    const urlPath = version ? `desktop/${version}` : 'desktop';
    return prepend(arch, urlPath, version || '');
};
const statusMessage = (err) => {
    return (err.statusCode
        ? [err.statusCode, err.statusMessage].join(' - ')
        : err.toString());
};
const prettyDownloadErr = (err, url) => {
    const msg = commonTags.stripIndent `
    URL: ${url}
    ${statusMessage(err)}
  `;
    debug$6(msg);
    return xvfb.throwFormErrorText(xvfb.errors.failedDownload)(msg);
};
/**
 * Checks checksum and file size for the given file. Allows both
 * values or just one of them to be checked.
 */
const verifyDownloadedFile = (filename, expectedSize, expectedChecksum) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
    if (expectedSize && expectedChecksum) {
        debug$6('verifying checksum and file size');
        return Bluebird.join(xvfb.util.getFileChecksum(filename), xvfb.util.getFileSize(filename), (checksum, filesize) => {
            if (checksum === expectedChecksum && filesize === expectedSize) {
                debug$6('downloaded file has the expected checksum and size ✅');
                return;
            }
            debug$6('raising error: checksum or file size mismatch');
            const text = commonTags.stripIndent `
          Corrupted download

          Expected downloaded file to have checksum: ${expectedChecksum}
          Computed checksum: ${checksum}

          Expected downloaded file to have size: ${expectedSize}
          Computed size: ${filesize}
        `;
            debug$6(text);
            throw new Error(text);
        });
    }
    if (expectedChecksum) {
        debug$6('only checking expected file checksum %d', expectedChecksum);
        const checksum = yield xvfb.util.getFileChecksum(filename);
        if (checksum === expectedChecksum) {
            debug$6('downloaded file has the expected checksum ✅');
            return;
        }
        debug$6('raising error: file checksum mismatch');
        const text = commonTags.stripIndent `
      Corrupted download

      Expected downloaded file to have checksum: ${expectedChecksum}
      Computed checksum: ${checksum}
    `;
        throw new Error(text);
    }
    if (expectedSize) {
        // maybe we don't have a checksum, but at least CDN returns content length
        // which we can check against the file size
        debug$6('only checking expected file size %d', expectedSize);
        const filesize = yield xvfb.util.getFileSize(filename);
        if (filesize === expectedSize) {
            debug$6('downloaded file has the expected size ✅');
            return;
        }
        debug$6('raising error: file size mismatch');
        const text = commonTags.stripIndent `
        Corrupted download

        Expected downloaded file to have size: ${expectedSize}
        Computed size: ${filesize}
      `;
        throw new Error(text);
    }
    debug$6('downloaded file lacks checksum or size to verify');
    return;
});
// downloads from given url
// return an object with
// {filename: ..., downloaded: true}
const downloadFromUrl = ({ url, downloadDestination, progress, ca, version, redirectTTL = defaultMaxRedirects }) => {
    if (redirectTTL <= 0) {
        return Promise.reject(new Error(commonTags.stripIndent `
          Failed downloading the Cypress binary.
          There were too many redirects. The default allowance is ${defaultMaxRedirects}.
          Maybe you got stuck in a redirect loop?
        `));
    }
    return new Bluebird((resolve, reject) => {
        const proxy = getProxyForUrlWithNpmConfig(url);
        debug$6('Downloading package', {
            url,
            proxy,
            downloadDestination,
        });
        if (ca) {
            debug$6('using custom CA details from npm config');
        }
        const reqOptions = Object.assign(Object.assign(Object.assign({ uri: url }, (proxy ? { proxy } : {})), (ca ? { agentOptions: { ca } } : {})), { method: 'GET', followRedirect: false });
        const req = request(reqOptions);
        // closure
        let started = null;
        let expectedSize;
        let expectedChecksum;
        requestProgress(req, {
            throttle: progress.throttle,
        })
            .on('response', (response) => {
            // we have computed checksum and filesize during test runner binary build
            // and have set it on the S3 object as user meta data, available via
            // these custom headers "x-amz-meta-..."
            // see https://github.com/cypress-io/cypress/pull/4092
            expectedSize = response.headers['x-amz-meta-size'] ||
                response.headers['content-length'];
            expectedChecksum = response.headers['x-amz-meta-checksum'];
            if (expectedChecksum) {
                debug$6('expected checksum %s', expectedChecksum);
            }
            if (expectedSize) {
                // convert from string (all Amazon custom headers are strings)
                expectedSize = Number(expectedSize);
                debug$6('expected file size %d', expectedSize);
            }
            // start counting now once we've gotten
            // response headers
            started = new Date();
            if (/^3/.test(response.statusCode)) {
                const redirectVersion = response.headers['x-version'];
                const redirectUrl = response.headers.location;
                debug$6('redirect version:', redirectVersion);
                debug$6('redirect url:', redirectUrl);
                downloadFromUrl({ url: redirectUrl, progress, ca, downloadDestination, version: redirectVersion, redirectTTL: redirectTTL - 1 })
                    .then(resolve).catch(reject);
                // if our status code does not start with 200
            }
            else if (!/^2/.test(response.statusCode)) {
                debug$6('response code %d', response.statusCode);
                const err = new Error(commonTags.stripIndent `
          Failed downloading the Cypress binary.
          Response code: ${response.statusCode}
          Response message: ${response.statusMessage}
        `);
                reject(err);
                // status codes here are all 2xx
            }
            else {
                // We only enable this pipe connection when we know we've got a successful return
                // and handle the completion with verify and resolve
                // there was a possible race condition between end of request and close of writeStream
                // that is made ordered with this Promise.all
                Bluebird.all([new Bluebird((r) => {
                        return response.pipe(fs.createWriteStream(downloadDestination).on('close', r));
                    }), new Bluebird((r) => response.on('end', r))])
                    .then(() => {
                    debug$6('downloading finished');
                    verifyDownloadedFile(downloadDestination, expectedSize, expectedChecksum)
                        .then(() => debug$6('verified'))
                        .then(() => resolve(version))
                        .catch(reject);
                });
            }
        })
            .on('error', (e) => {
            if (e.code === 'ECONNRESET')
                return; // sometimes proxies give ECONNRESET but we don't care
            reject(e);
        })
            .on('progress', (state) => {
            // total time we've elapsed
            // starting on our first progress notification
            const elapsed = +new Date() - +started;
            // request-progress sends a value between 0 and 1
            const percentage = xvfb.util.convertPercentToPercentage(state.percent);
            const eta = xvfb.util.calculateEta(percentage, elapsed);
            // send up our percent and seconds remaining
            progress.onProgress(percentage, xvfb.util.secsRemaining(eta));
        });
    });
};
/**
 * Download Cypress.zip from external versionUrl to local file.
 * @param [string] version Could be "3.3.0" or full URL
 * @param [string] downloadDestination Local filename to save as
 */
const start$3 = (opts) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
    let { version, downloadDestination, progress, redirectTTL } = opts;
    if (!downloadDestination) {
        assert.ok(_.isString(downloadDestination) && !_.isEmpty(downloadDestination), 'missing download dir');
    }
    if (!progress) {
        progress = { onProgress: () => {
                return {};
            } };
    }
    const arch = yield xvfb.util.getRealArch();
    const versionUrl = getUrl(arch, version);
    progress.throttle = 100;
    debug$6('needed Cypress version: %s', version);
    debug$6('source url %s', versionUrl);
    debug$6(`downloading cypress.zip to "${downloadDestination}"`);
    try {
        // ensure download dir exists
        yield fs.ensureDir(path.dirname(downloadDestination));
        const ca = yield getCA();
        return downloadFromUrl(Object.assign({ url: versionUrl, downloadDestination, progress, ca, version }, (redirectTTL ? { redirectTTL } : {})));
    }
    catch (err) {
        return prettyDownloadErr(err, versionUrl);
    }
});
const downloadModule = {
    start: start$3,
    getUrl,
    getProxyForUrlWithNpmConfig,
    getCA,
};

const debug$5 = Debug('cypress:cli:unzip');
const unzipTools = {
    extract,
};
// expose this function for simple testing
const unzip = (_a) => xvfb.__awaiter(void 0, [_a], void 0, function* ({ zipFilePath, installDir, progress }) {
    debug$5('unzipping from %s', zipFilePath);
    debug$5('into', installDir);
    if (!zipFilePath) {
        throw new Error('Missing zip filename');
    }
    const startTime = Date.now();
    let yauzlDoneTime = 0;
    yield fs.ensureDir(installDir);
    yield new Promise((resolve, reject) => {
        return yauzl.open(zipFilePath, (err, zipFile) => {
            yauzlDoneTime = Date.now();
            if (err) {
                debug$5('error using yauzl %s', err.message);
                return reject(err);
            }
            const total = zipFile.entryCount;
            debug$5('zipFile entries count', total);
            const started = new Date();
            let percent = 0;
            let count = 0;
            const notify = (percent) => {
                const elapsed = +new Date() - +started;
                const eta = xvfb.util.calculateEta(percent, elapsed);
                progress.onProgress(percent, xvfb.util.secsRemaining(eta));
            };
            const tick = () => {
                count += 1;
                percent = ((count / total) * 100);
                const displayPercent = percent.toFixed(0);
                return notify(Number(displayPercent));
            };
            const unzipWithNode = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
                debug$5('unzipping with node.js (slow)');
                const opts = {
                    dir: installDir,
                    onEntry: tick,
                };
                debug$5('calling Node extract tool %s %o', zipFilePath, opts);
                try {
                    yield unzipTools.extract(zipFilePath, opts);
                    debug$5('node unzip finished');
                    return resolve();
                }
                catch (err) {
                    const error = err || new Error('Unknown error with Node extract tool');
                    debug$5('error %s', error.message);
                    return reject(error);
                }
            });
            const unzipFallback = _.once(unzipWithNode);
            const unzipWithUnzipTool = () => {
                debug$5('unzipping via `unzip`');
                const inflatingRe = /inflating:/;
                const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir]);
                sp.on('error', (err) => {
                    debug$5('unzip tool error: %s', err.message);
                    unzipFallback();
                });
                sp.on('close', (code) => {
                    debug$5('unzip tool close with code %d', code);
                    if (code === 0) {
                        percent = 100;
                        notify(percent);
                        return resolve();
                    }
                    debug$5('`unzip` failed %o', { code });
                    return unzipFallback();
                });
                sp.stdout.on('data', (data) => {
                    if (inflatingRe.test(data)) {
                        return tick();
                    }
                });
                sp.stderr.on('data', (data) => {
                    debug$5('`unzip` stderr %s', data);
                });
            };
            // we attempt to first unzip with the native osx
            // ditto because its less likely to have problems
            // with corruption, symlinks, or icons causing failures
            // and can handle resource forks
            // http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/
            const unzipWithOsx = () => {
                debug$5('unzipping via `ditto`');
                const copyingFileRe = /^copying file/;
                const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir]);
                // f-it just unzip with node
                sp.on('error', (err) => {
                    debug$5(err.message);
                    unzipFallback();
                });
                sp.on('close', (code) => {
                    if (code === 0) {
                        // make sure we get to 100% on the progress bar
                        // because reading in lines is not really accurate
                        percent = 100;
                        notify(percent);
                        return resolve();
                    }
                    debug$5('`ditto` failed %o', { code });
                    return unzipFallback();
                });
                return readline.createInterface({
                    input: sp.stderr,
                })
                    .on('line', (line) => {
                    if (copyingFileRe.test(line)) {
                        return tick();
                    }
                });
            };
            switch (os.platform()) {
                case 'darwin':
                    return unzipWithOsx();
                case 'linux':
                    return unzipWithUnzipTool();
                case 'win32':
                    return unzipWithNode();
                default:
                    return;
            }
        });
    });
    debug$5('unzip completed %o', {
        yauzlMs: yauzlDoneTime - startTime,
        unzipMs: Date.now() - yauzlDoneTime,
    });
});
function isMaybeWindowsMaxPathLengthError(err) {
    return os.platform() === 'win32' && err.code === 'ENOENT' && err.syscall === 'realpath';
}
const start$2 = (_a) => xvfb.__awaiter(void 0, [_a], void 0, function* ({ zipFilePath, installDir, progress }) {
    assert.ok(_.isString(installDir) && !_.isEmpty(installDir), 'missing installDir');
    if (!progress) {
        progress = { onProgress: () => {
                return {};
            } };
    }
    try {
        const installDirExists = yield fs.pathExists(installDir);
        if (installDirExists) {
            debug$5('removing existing unzipped binary', installDir);
            yield fs.remove(installDir);
        }
        yield unzip({ zipFilePath, installDir, progress });
    }
    catch (err) {
        const errorTemplate = isMaybeWindowsMaxPathLengthError(err) ?
            xvfb.errors.failedUnzipWindowsMaxPathLength
            : xvfb.errors.failedUnzip;
        yield xvfb.throwFormErrorText(errorTemplate)(err);
    }
});
const unzipModule = {
    start: start$2,
    utils: {
        unzip,
        unzipTools,
    },
};

const debug$4 = Debug('cypress:cli');
function _getBinaryUrlFromBuildInfo(version, arch, { commitSha, commitBranch }) {
    const platform = os.platform();
    if ((platform === 'win32') && (arch === 'arm64')) {
        debug$4(`detected platform ${platform} architecture ${arch} combination`);
        arch = 'x64';
        debug$4(`overriding to download ${platform}-${arch} pre-release binary instead`);
    }
    return `https://cdn.cypress.io/beta/binary/${version}/${platform}-${arch}/${commitBranch}-${commitSha}/cypress.zip`;
}
const alreadyInstalledMsg = () => {
    if (!xvfb.util.isPostInstall()) {
        xvfb.loggerModule.log(commonTags.stripIndent `
      Skipping installation:

        Pass the ${chalk.yellow('--force')} option if you'd like to reinstall anyway.
    `);
    }
};
const displayCompletionMsg = () => {
    // check here to see if we are globally installed
    if (xvfb.util.isInstalledGlobally()) {
        // if we are display a warning
        xvfb.loggerModule.log();
        xvfb.loggerModule.warn(commonTags.stripIndent `
      ${logSymbols.warning} Warning: It looks like you\'ve installed Cypress globally.

        The recommended way to install Cypress is as a devDependency per project.

        You should probably run these commands:

        - ${chalk.cyan('npm uninstall -g cypress')}
        - ${chalk.cyan('npm install --save-dev cypress')}
    `);
        return;
    }
    xvfb.loggerModule.log();
    xvfb.loggerModule.log('You can now open Cypress by running one of the following, depending on your package manager:');
    xvfb.loggerModule.log();
    xvfb.loggerModule.log(chalk.cyan('- npx cypress open'));
    xvfb.loggerModule.log(chalk.cyan('- yarn cypress open'));
    xvfb.loggerModule.log(chalk.cyan('- pnpm cypress open'));
    xvfb.loggerModule.log();
    xvfb.loggerModule.log(chalk.grey('https://on.cypress.io/opening-the-app'));
    xvfb.loggerModule.log();
};
const downloadAndUnzip = ({ version, installDir, downloadDir }) => {
    const progress = {
        throttle: 100,
        onProgress: null,
    };
    const downloadDestination = path.join(downloadDir, `cypress-${process.pid}.zip`);
    const rendererOptions = getRendererOptions();
    // let the user know what version of cypress we're downloading!
    xvfb.loggerModule.log(`Installing Cypress ${chalk.gray(`(version: ${version})`)}`);
    xvfb.loggerModule.log();
    const tasks = new listr2.Listr([
        {
            options: { title: xvfb.util.titleize('Downloading Cypress') },
            task: (ctx, task) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
                // as our download progresses indicate the status
                progress.onProgress = progessify(task, 'Downloading Cypress');
                const redirectVersion = yield downloadModule.start({ version, downloadDestination, progress });
                if (redirectVersion)
                    version = redirectVersion;
                debug$4(`finished downloading file: ${downloadDestination}`);
                // save the download destination for unzipping
                xvfb.util.setTaskTitle(task, xvfb.util.titleize(chalk.green('Downloaded Cypress')), rendererOptions.renderer);
            }),
        },
        unzipTask({
            progress,
            zipFilePath: downloadDestination,
            installDir,
            rendererOptions,
        }),
        {
            options: { title: xvfb.util.titleize('Finishing Installation') },
            task: (ctx, task) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
                const cleanup = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
                    debug$4('removing zip file %s', downloadDestination);
                    yield fs.remove(downloadDestination);
                });
                yield cleanup();
                debug$4('finished installation in', installDir);
                xvfb.util.setTaskTitle(task, xvfb.util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir)), rendererOptions.renderer);
            }),
        },
    ], { rendererOptions });
    // start the tasks!
    return tasks.run();
};
const validateOS = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
    const platformInfo = yield xvfb.util.getPlatformInfo();
    return platformInfo.match(/(win32-x64|win32-arm64|linux-x64|linux-arm64|darwin-x64|darwin-arm64)/);
});
/**
 * Returns the version to install - either a string like `1.2.3` to be fetched
 * from the download server or a file path or HTTP URL.
 */
function getVersionOverride(version, { arch, envVarVersion, buildInfo }) {
    // let this environment variable reset the binary version we need
    if (envVarVersion) {
        return envVarVersion;
    }
    if (buildInfo && !buildInfo.stable) {
        xvfb.loggerModule.log(chalk.yellow(commonTags.stripIndent `
        ${logSymbols.warning} Warning: You are installing a pre-release build of Cypress.

        Bugs may be present which do not exist in production builds.

        This build was created from:
          * Commit SHA: ${buildInfo.commitSha}
          * Commit Branch: ${buildInfo.commitBranch}
          * Commit Timestamp: ${buildInfo.commitDate}
      `));
        xvfb.loggerModule.log();
        return _getBinaryUrlFromBuildInfo(version, arch, buildInfo);
    }
}
function getEnvVarVersion() {
    if (!xvfb.util.getEnv('CYPRESS_INSTALL_BINARY'))
        return;
    // because passed file paths are often double quoted
    // and might have extra whitespace around, be robust and trim the string
    const trimAndRemoveDoubleQuotes = true;
    const envVarVersion = xvfb.util.getEnv('CYPRESS_INSTALL_BINARY', trimAndRemoveDoubleQuotes);
    debug$4('using environment variable CYPRESS_INSTALL_BINARY "%s"', envVarVersion);
    return envVarVersion;
}
const start$1 = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
    debug$4('installing with options %j', options);
    const envVarVersion = getEnvVarVersion();
    if (envVarVersion === '0') {
        debug$4('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install');
        xvfb.loggerModule.log(commonTags.stripIndent `
        ${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`);
        xvfb.loggerModule.log();
        return;
    }
    const pkgPath = xvfb.relativeToRepoRoot('package.json');
    if (!pkgPath) {
        return xvfb.throwFormErrorText('Could not find package.json for Cypress package to determine build information')();
    }
    const { buildInfo, version } = JSON.parse(yield promises.readFile(pkgPath, 'utf8'));
    _.defaults(options, {
        force: false,
        buildInfo,
    });
    if (xvfb.util.getEnv('CYPRESS_CACHE_FOLDER')) {
        const envCache = xvfb.util.getEnv('CYPRESS_CACHE_FOLDER');
        xvfb.loggerModule.log(commonTags.stripIndent `
        ${chalk.yellow('Note:')} Overriding Cypress cache directory to: ${chalk.cyan(envCache)}

              Previous installs of Cypress may not be found.
      `);
        xvfb.loggerModule.log();
    }
    const pkgVersion = xvfb.util.pkgVersion();
    const arch = yield xvfb.util.getRealArch();
    const versionOverride = getVersionOverride(version, { arch, envVarVersion, buildInfo: options.buildInfo });
    const versionToInstall = versionOverride || pkgVersion;
    debug$4('version in package.json is %s, version to install is %s', pkgVersion, versionToInstall);
    const installDir = xvfb.stateModule.getVersionDir(pkgVersion, options.buildInfo);
    const cacheDir = xvfb.stateModule.getCacheDir();
    const binaryDir = xvfb.stateModule.getBinaryDir(pkgVersion);
    if (!(yield validateOS())) {
        return xvfb.throwFormErrorText(xvfb.errors.invalidOS)();
    }
    try {
        yield fs.ensureDir(cacheDir);
    }
    catch (err) {
        if (err.code === 'EACCES') {
            return xvfb.throwFormErrorText(xvfb.errors.invalidCacheDirectory)(commonTags.stripIndent `
        Failed to access ${chalk.cyan(cacheDir)}:

        ${err.message}
      `);
        }
        throw err;
    }
    const binaryPkg = yield xvfb.stateModule.getBinaryPkgAsync(binaryDir);
    const binaryVersion = yield xvfb.stateModule.getBinaryPkgVersion(binaryPkg);
    const shouldInstall = () => {
        if (!binaryVersion) {
            debug$4('no binary installed under cli version');
            return true;
        }
        xvfb.loggerModule.log();
        xvfb.loggerModule.log(commonTags.stripIndent `
      Cypress ${chalk.green(binaryVersion)} is installed in ${chalk.cyan(installDir)}
      `);
        xvfb.loggerModule.log();
        if (options.force) {
            debug$4('performing force install over existing binary');
            return true;
        }
        if ((binaryVersion === versionToInstall) || !xvfb.util.isSemver(versionToInstall)) {
            // our version matches, tell the user this is a noop
            alreadyInstalledMsg();
            return false;
        }
        return true;
    };
    // noop if we've been told not to download
    if (!shouldInstall()) {
        return debug$4('Not downloading or installing binary');
    }
    if (envVarVersion) {
        xvfb.loggerModule.log(chalk.yellow(commonTags.stripIndent `
        ${logSymbols.warning} Warning: Forcing a binary version different than the default.

          The CLI expected to install version: ${chalk.green(pkgVersion)}

          Instead we will install version: ${chalk.green(versionToInstall)}

          These versions may not work properly together.
      `));
        xvfb.loggerModule.log();
    }
    const getLocalFilePath = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
        // see if version supplied is a path to a binary
        if (yield fs.pathExists(versionToInstall)) {
            return path.extname(versionToInstall) === '.zip' ? versionToInstall : false;
        }
        const possibleFile = xvfb.util.formAbsolutePath(versionToInstall);
        debug$4('checking local file', possibleFile, 'cwd', process.cwd());
        // if this exists return the path to it
        // else false
        if ((yield fs.pathExists(possibleFile)) && path.extname(possibleFile) === '.zip') {
            return possibleFile;
        }
        return false;
    });
    const pathToLocalFile = yield getLocalFilePath();
    if (pathToLocalFile) {
        const absolutePath = path.resolve(versionToInstall);
        debug$4('found local file at', absolutePath);
        debug$4('skipping download');
        const rendererOptions = getRendererOptions();
        return new listr2.Listr([unzipTask({
                progress: {
                    throttle: 100,
                    onProgress: null,
                },
                zipFilePath: absolutePath,
                installDir,
                rendererOptions,
            })], { rendererOptions }).run();
    }
    if (options.force) {
        debug$4('Cypress already installed at', installDir);
        debug$4('but the installation was forced');
    }
    debug$4('preparing to download and unzip version ', versionToInstall, 'to path', installDir);
    const downloadDir = os.tmpdir();
    yield downloadAndUnzip({ version: versionToInstall, installDir, downloadDir });
    // delay 1 sec for UX, unless we are testing
    yield timers.setTimeout(1000);
    displayCompletionMsg();
});
const unzipTask = ({ zipFilePath, installDir, progress, rendererOptions }) => {
    return {
        options: { title: xvfb.util.titleize('Unzipping Cypress') },
        task: (ctx, task) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
            // as our unzip progresses indicate the status
            progress.onProgress = progessify(task, 'Unzipping Cypress');
            yield unzipModule.start({ zipFilePath, installDir, progress });
            xvfb.util.setTaskTitle(task, xvfb.util.titleize(chalk.green('Unzipped Cypress')), rendererOptions.renderer);
        }),
    };
};
const progessify = (task, title) => {
    // return higher order function
    return (percentComplete, remaining) => {
        const percentCompleteStr = chalk.white(` ${percentComplete}%`);
        // pluralize seconds remaining
        const remainingStr = chalk.gray(`${remaining}s`);
        xvfb.util.setTaskTitle(task, xvfb.util.titleize(title, percentCompleteStr, remainingStr), getRendererOptions().renderer);
    };
};
// if we are running in CI then use
// the verbose renderer else use
// the default
const getRendererOptions = () => {
    let renderer = xvfb.util.isCi() ? spawn.VerboseRenderer : 'default';
    if (xvfb.loggerModule.logLevel() === 'silent') {
        renderer = 'silent';
    }
    return {
        renderer,
    };
};
var installModule = {
    start: start$1,
    _getBinaryUrlFromBuildInfo,
};

/**
 * Throws an error with "details" property from
 * "errors" object.
 * @param {Object} details - Error details
 */
const throwInvalidOptionError = (details) => {
    if (!details) {
        details = xvfb.errors.unknownError;
    }
    // throw this error synchronously, it will be caught later on and
    // the details will be propagated to the promise chain
    const err = new Error();
    err.details = details;
    throw err;
};
/**
 * Selects exec args based on the configured `testingType`
 * @param {string} testingType The type of tests being executed
 * @returns {string[]} The array of new exec arguments
 */
const processTestingType = (options) => {
    if (options.e2e && options.component) {
        return throwInvalidOptionError(xvfb.errors.incompatibleTestTypeFlags);
    }
    if (options.testingType && (options.component || options.e2e)) {
        return throwInvalidOptionError(xvfb.errors.incompatibleTestTypeFlags);
    }
    if (options.testingType === 'component' || options.component || options.ct) {
        return ['--testing-type', 'component'];
    }
    if (options.testingType === 'e2e' || options.e2e) {
        return ['--testing-type', 'e2e'];
    }
    if (options.testingType) {
        return throwInvalidOptionError(xvfb.errors.invalidTestingType);
    }
    return [];
};
/**
 * Throws an error if configFile is string 'false' or boolean false
 * @param {*} options
 */
const checkConfigFile = (options) => {
    // CLI will parse as string, module API can pass in boolean
    if (options.configFile === 'false' || options.configFile === false) {
        throwInvalidOptionError(xvfb.errors.invalidConfigFile);
    }
};

const debug$3 = Debug('cypress:cli');
/**
 * Maps options collected by the CLI
 * and forms list of CLI arguments to the server.
 *
 * Note: there is lightweight validation, with errors
 * thrown synchronously.
 *
 * @returns {string[]} list of CLI arguments
 */
const processOpenOptions = (options = {}) => {
    // In addition to setting the project directory, setting the project option
    // here ultimately decides whether cypress is run in global mode or not.
    // It's first based off whether it's installed globally by npm/yarn (-g).
    // A global install can be overridden by the --project flag, putting Cypress
    // in project mode. A non-global install can be overridden by the --global
    // flag, putting it in global mode.
    if (!xvfb.util.isInstalledGlobally() && !options.global && !options.project) {
        options.project = process.cwd();
    }
    const args = [];
    if (options.config) {
        args.push('--config', options.config);
    }
    if (options.configFile !== undefined) {
        checkConfigFile(options);
        args.push('--config-file', options.configFile);
    }
    if (options.browser) {
        args.push('--browser', options.browser);
    }
    if (options.env) {
        args.push('--env', options.env);
    }
    if (options.expose) {
        args.push('--expose', options.expose);
    }
    if (options.port) {
        args.push('--port', options.port);
    }
    if (options.project) {
        args.push('--project', options.project);
    }
    if (options.global) {
        args.push('--global', options.global);
    }
    if (options.inspect) {
        args.push('--inspect');
    }
    if (options.inspectBrk) {
        args.push('--inspectBrk');
    }
    args.push(...processTestingType(options));
    debug$3('opening from options %j', options);
    debug$3('command line arguments %j', args);
    return args;
};
const start = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
    function open() {
        try {
            const args = processOpenOptions(options);
            return spawn.start$1(args, {
                dev: options.dev,
                detached: Boolean(options.detached),
            });
        }
        catch (err) {
            if (err.details) {
                return xvfb.exitWithError(err.details)();
            }
            throw err;
        }
    }
    if (options.dev) {
        return open();
    }
    yield spawn.start();
    return open();
});
var openModule = {
    start,
    processOpenOptions,
};

const debug$2 = Debug('cypress:cli:run');
/**
 * Typically a user passes a string path to the project.
 * But "cypress open" allows using `false` to open in global mode,
 * and the user can accidentally execute `cypress run --project false`
 * which should be invalid.
 */
const isValidProject = (v) => {
    if (typeof v === 'boolean') {
        return false;
    }
    if (v === '' || v === 'false' || v === 'true') {
        return false;
    }
    return true;
};
/**
 * Maps options collected by the CLI
 * and forms list of CLI arguments to the server.
 *
 * Note: there is lightweight validation, with errors
 * thrown synchronously.
 *
 * @returns {string[]} list of CLI arguments
 */
const processRunOptions = (options = {}) => {
    debug$2('processing run options %o', options);
    if (!isValidProject(options.project)) {
        debug$2('invalid project option %o', { project: options.project });
        return throwInvalidOptionError(xvfb.errors.invalidRunProjectPath);
    }
    const args = ['--run-project', options.project];
    if (options.autoCancelAfterFailures || options.autoCancelAfterFailures === 0 || options.autoCancelAfterFailures === false) {
        args.push('--auto-cancel-after-failures', options.autoCancelAfterFailures);
    }
    if (options.browser) {
        args.push('--browser', options.browser);
    }
    if (options.ciBuildId) {
        args.push('--ci-build-id', options.ciBuildId);
    }
    if (options.config) {
        args.push('--config', options.config);
    }
    if (options.configFile !== undefined) {
        checkConfigFile(options);
        args.push('--config-file', options.configFile);
    }
    if (options.env) {
        args.push('--env', options.env);
    }
    if (options.expose) {
        args.push('--expose', options.expose);
    }
    if (options.exit === false) {
        args.push('--no-exit');
    }
    if (options.group) {
        args.push('--group', options.group);
    }
    if (options.headed) {
        args.push('--headed', options.headed);
    }
    if (options.headless) {
        if (options.headed) {
            return throwInvalidOptionError(xvfb.errors.incompatibleHeadlessFlags);
        }
        args.push('--headed', String(!options.headless));
    }
    // if key is set use that - else attempt to find it by environment variable
    if (options.key == null) {
        debug$2('--key is not set, looking up environment variable CYPRESS_RECORD_KEY');
        options.key = xvfb.util.getEnv('CYPRESS_RECORD_KEY');
    }
    // if we have a key assume we're in record mode
    if (options.key) {
        args.push('--key', options.key);
    }
    if (options.outputPath) {
        args.push('--output-path', options.outputPath);
    }
    if (options.parallel) {
        args.push('--parallel');
    }
    if (options.passWithNoTests) {
        args.push('--pass-with-no-tests');
    }
    if (options.posixExitCodes) {
        args.push('--posix-exit-codes');
    }
    if (options.port) {
        args.push('--port', options.port);
    }
    if (options.quiet) {
        args.push('--quiet');
    }
    // if record is defined and we're not
    // already in ci mode, then send it up
    if (options.record != null) {
        args.push('--record', options.record);
    }
    // if we have a specific reporter push that into the args
    if (options.reporter) {
        args.push('--reporter', options.reporter);
    }
    // if we have a specific reporter push that into the args
    if (options.reporterOptions) {
        args.push('--reporter-options', options.reporterOptions);
    }
    if (options.runnerUi != null) {
        args.push('--runner-ui', options.runnerUi);
    }
    // if we have specific spec(s) push that into the args
    if (options.spec) {
        args.push('--spec', options.spec);
    }
    if (options.tag) {
        args.push('--tag', options.tag);
    }
    if (options.inspect) {
        args.push('--inspect');
    }
    if (options.inspectBrk) {
        args.push('--inspectBrk');
    }
    args.push(...processTestingType(options));
    return args;
};
const runModule = {
    processRunOptions,
    isValidProject,
    // resolves with the number of failed tests
    start() {
        return xvfb.__awaiter(this, arguments, void 0, function* (options = {}) {
            _.defaults(options, {
                key: null,
                spec: null,
                reporter: null,
                reporterOptions: null,
                project: process.cwd(),
            });
            function run() {
                try {
                    const args = processRunOptions(options);
                    debug$2('run to spawn.start args %j', args);
                    return spawn.start$1(args, {
                        dev: options.dev,
                    });
                }
                catch (err) {
                    if (err.details) {
                        return xvfb.exitWithError(err.details)();
                    }
                    throw err;
                }
            }
            if (options.dev) {
                return run();
            }
            yield spawn.start();
            return run();
        });
    },
};

/**
 * Get the size of a folder or a file.
 *
 * This function returns the actual file size of the folder (size), not the allocated space on disk (size on disk).
 * For more details between the difference, check this link:
 * https://www.howtogeek.com/180369/why-is-there-a-big-difference-between-size-and-size-on-disk/
 *
 * @param {string} path path to the file or the folder.
 */
function getSize(path$1) {
    return xvfb.__awaiter(this, void 0, void 0, function* () {
        const stat = yield fs.lstat(path$1);
        if (stat.isDirectory()) {
            const list = yield fs.readdir(path$1);
            return Bluebird.resolve(list).reduce((prev, curr) => xvfb.__awaiter(this, void 0, void 0, function* () {
                const currPath = path.join(path$1, curr);
                const s = yield fs.lstat(currPath);
                if (s.isDirectory()) {
                    return prev + (yield getSize(currPath));
                }
                return prev + s.size;
            }), 0);
        }
        return stat.size;
    });
}

dayjs.extend(relativeTime);
// output colors for the table
const colors = {
    titles: chalk.white,
    dates: chalk.cyan,
    values: chalk.green,
    size: chalk.gray,
};
const logCachePath = () => {
    xvfb.loggerModule.always(xvfb.stateModule.getCacheDir());
    return undefined;
};
const clear = () => {
    return fs.remove(xvfb.stateModule.getCacheDir());
};
const prune = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
    const cacheDir = xvfb.stateModule.getCacheDir();
    const checkedInBinaryVersion = xvfb.util.pkgVersion();
    let deletedBinary = false;
    try {
        const versions = yield fs.readdir(cacheDir);
        for (const version of versions) {
            if (version !== checkedInBinaryVersion) {
                deletedBinary = true;
                const versionDir = path.join(cacheDir, version);
                yield fs.remove(versionDir);
            }
        }
        if (deletedBinary) {
            xvfb.loggerModule.always(`Deleted all binary caches except for the ${checkedInBinaryVersion} binary cache.`);
        }
        else {
            xvfb.loggerModule.always(`No binary caches found to prune.`);
        }
    }
    catch (e) {
        if (e.code === 'ENOENT') {
            xvfb.loggerModule.always(`No Cypress cache was found at ${cacheDir}. Nothing to prune.`);
            return;
        }
        throw e;
    }
});
const fileSizeInMB = (size) => {
    return `${(size / 1024 / 1024).toFixed(1)}MB`;
};
/**
 * Collects all cached versions, finds when each was used
 * and prints a table with results to the terminal
 */
const list = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (showSize = false) {
    const binaries = yield getCachedVersions(showSize);
    const head = [colors.titles('version'), colors.titles('last used')];
    if (showSize) {
        head.push(colors.titles('size'));
    }
    const table = new Table({
        head,
    });
    binaries.forEach((binary) => {
        const versionString = colors.values(binary.version);
        const lastUsed = binary.accessed ? colors.dates(binary.accessed) : 'unknown';
        const row = [versionString, lastUsed];
        if (showSize) {
            const size = colors.size(fileSizeInMB(binary.size));
            row.push(size);
        }
        return table.push(row);
    });
    xvfb.loggerModule.always(table.toString());
});
const getCachedVersions = (showSize) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
    const cacheDir = xvfb.stateModule.getCacheDir();
    const versions = yield fs.readdir(cacheDir);
    const filteredVersions = versions.filter(xvfb.util.isSemver).map((version) => {
        return {
            version,
            folderPath: path.join(cacheDir, version),
        };
    });
    const binaries = [];
    for (const binary of filteredVersions) {
        const binaryDir = xvfb.stateModule.getBinaryDir(binary.version);
        const executable = xvfb.stateModule.getPathToExecutable(binaryDir);
        try {
            const stat = yield fs.stat(executable);
            const lastAccessedTime = _.get(stat, 'atime');
            if (lastAccessedTime) {
                const accessed = dayjs(lastAccessedTime).fromNow();
                // @ts-expect-error - accessed is not defined in the type
                binary.accessed = accessed;
            }
            // if no lastAccessedTime
            // the test runner has never been opened
            // or could be a test simulating missing timestamp
        }
        catch (e) {
            // could not find the binary or gets its stats
            // no-op
        }
        if (showSize) {
            const binaryDir = xvfb.stateModule.getBinaryDir(binary.version);
            const size = yield getSize(binaryDir);
            binaries.push(Object.assign(Object.assign({}, binary), { size }));
        }
        else {
            binaries.push(binary);
        }
    }
    return binaries;
});
const cacheModule = {
    path: logCachePath,
    clear,
    prune,
    list,
    getCachedVersions,
};

const debug$1 = Debug('cypress:cli');
const getBinaryDirectory = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
    if (xvfb.util.getEnv('CYPRESS_RUN_BINARY')) {
        let envBinaryPath = path.resolve(xvfb.util.getEnv('CYPRESS_RUN_BINARY'));
        try {
            const envBinaryDir = yield xvfb.stateModule.parseRealPlatformBinaryFolderAsync(envBinaryPath);
            if (!envBinaryDir) {
                const raiseErrorFn = xvfb.throwFormErrorText(xvfb.errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath));
                yield raiseErrorFn();
            }
            debug$1('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir);
            return envBinaryDir;
        }
        catch (err) {
            const raiseErrorFn = xvfb.throwFormErrorText(xvfb.errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath));
            yield raiseErrorFn(err.message);
        }
    }
    return xvfb.stateModule.getBinaryDir();
});
const getVersions = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
    const binDir = yield getBinaryDirectory();
    const pkg = yield xvfb.stateModule.getBinaryPkgAsync(binDir);
    const versions = {
        binary: xvfb.stateModule.getBinaryPkgVersion(pkg),
        electronVersion: xvfb.stateModule.getBinaryElectronVersion(pkg),
        electronNodeVersion: xvfb.stateModule.getBinaryElectronNodeVersion(pkg),
    };
    debug$1('binary versions %o', versions);
    const buildInfo = xvfb.util.pkgBuildInfo();
    let packageVersion = xvfb.util.pkgVersion();
    if (!buildInfo)
        packageVersion += ' (development)';
    else if (!buildInfo.stable)
        packageVersion += ' (pre-release)';
    const versionsFinal = {
        package: packageVersion,
        binary: versions.binary || 'not installed',
        electronVersion: versions.electronVersion || 'not found',
        electronNodeVersion: versions.electronNodeVersion || 'not found',
    };
    debug$1('combined versions %o', versions);
    return versionsFinal;
});
const versionsModule = {
    getVersions,
};

// color for numbers and show values
const g = chalk.green;
// color for paths
const p = chalk.cyan;
const red = chalk.red;
// urls
const link = chalk.blue.underline;
// to be exported
const methods = {};
methods.findProxyEnvironmentVariables = () => {
    return _.pick(process.env, ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY']);
};
const maskSensitiveVariables = (obj) => {
    const masked = Object.assign({}, obj);
    if (masked.CYPRESS_RECORD_KEY) {
        masked.CYPRESS_RECORD_KEY = '<redacted>';
    }
    return masked;
};
methods.findCypressEnvironmentVariables = () => {
    const isCyVariable = (val, key) => key.startsWith('CYPRESS_');
    return _.pickBy(process.env, isCyVariable);
};
const formatCypressVariables = () => {
    const vars = methods.findCypressEnvironmentVariables();
    return maskSensitiveVariables(vars);
};
methods.start = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
    const args = ['--mode=info'];
    yield spawn.start$1(args, {
        dev: options.dev,
    });
    console.log();
    const proxyVars = methods.findProxyEnvironmentVariables();
    if (_.isEmpty(proxyVars)) {
        console.log('Proxy Settings: none detected');
    }
    else {
        console.log('Proxy Settings:');
        _.forEach(proxyVars, (value, key) => {
            console.log('%s: %s', key, g(value));
        });
        console.log();
        console.log('Learn More: %s', link('https://on.cypress.io/proxy-configuration'));
        console.log();
    }
    const cyVars = formatCypressVariables();
    if (_.isEmpty(cyVars)) {
        console.log('Environment Variables: none detected');
    }
    else {
        console.log('Environment Variables:');
        _.forEach(cyVars, (value, key) => {
            console.log('%s: %s', key, g(value));
        });
    }
    console.log();
    console.log('Application Data:', p(xvfb.util.getApplicationDataFolder()));
    console.log('Browser Profiles:', p(xvfb.util.getApplicationDataFolder('browsers')));
    console.log('Binary Caches: %s', p(xvfb.stateModule.getCacheDir()));
    console.log();
    const osVersion = yield xvfb.util.getOsVersionAsync();
    const buildInfo = xvfb.util.pkgBuildInfo();
    const isStable = buildInfo && buildInfo.stable;
    console.log('Cypress Version: %s', g(xvfb.util.pkgVersion()), isStable ? g('(stable)') : red('(pre-release)'));
    console.log('System Platform: %s (%s)', g(os.platform()), g(osVersion));
    console.log('System Memory: %s free %s', g(prettyBytes(os.totalmem())), g(prettyBytes(os.freemem())));
    if (!buildInfo) {
        console.log();
        console.log('This is the', red('development'), '(un-built) Cypress CLI.');
    }
    else if (!isStable) {
        console.log();
        console.log('This is a', red('pre-release'), 'build of Cypress.');
        console.log('Build info:');
        console.log('  Commit SHA:', g(buildInfo.commitSha));
        console.log('  Commit Branch:', g(buildInfo.commitBranch));
        console.log('  Commit Date:', g(buildInfo.commitDate));
    }
});

const debug = Debug('cypress:cli:cli');
// patch "commander" method called when a user passed an unknown option
// we want to print help for the current command and exit with an error
function unknownOption(flag, type = 'option') {
    if (this._allowUnknownOption)
        return;
    xvfb.loggerModule.error();
    xvfb.loggerModule.error(`  error: unknown ${type}:`, flag);
    xvfb.loggerModule.error();
    this.outputHelp();
    process.exit(1);
}
commander.Command.prototype.unknownOption = unknownOption;
const coerceFalse = (arg) => {
    return arg !== 'false';
};
const coerceAnyStringToInt = (arg) => {
    return typeof arg === 'string' ? parseInt(arg) : arg;
};
const spaceDelimitedArgsMsg = (flag, args) => {
    let msg = `
    ${logSymbols.warning} Warning: It looks like you're passing --${flag} a space-separated list of arguments:

    "${args.join(' ')}"

    This will work, but it's not recommended.

    If you are trying to pass multiple arguments, separate them with commas instead:
      cypress run --${flag} arg1,arg2,arg3
  `;
    if (flag === 'spec') {
        msg += `
    The most common cause of this warning is using an unescaped glob pattern. If you are
    trying to pass a glob pattern, escape it using quotes:
      cypress run --spec "**/*.spec.js"
    `;
    }
    xvfb.loggerModule.log();
    xvfb.loggerModule.warn(commonTags.stripIndent(msg));
    xvfb.loggerModule.log();
};
const parseVariableOpts = (fnArgs, args) => {
    const [opts, unknownArgs] = fnArgs;
    if ((unknownArgs && unknownArgs.length) && (opts.spec || opts.tag)) {
        // this will capture space-delimited args after
        // flags that could have possible multiple args
        // but before the next option
        // --spec spec1 spec2 or --tag foo bar
        const multiArgFlags = _.compact([
            opts.spec ? 'spec' : opts.spec,
            opts.tag ? 'tag' : opts.tag,
        ]);
        _.forEach(multiArgFlags, (flag) => {
            const argIndex = _.indexOf(args, `--${flag}`) + 2;
            const nextOptOffset = _.findIndex(_.slice(args, argIndex), (arg) => {
                return _.startsWith(arg, '--');
            });
            const endIndex = nextOptOffset !== -1 ? argIndex + nextOptOffset : args.length;
            const maybeArgs = _.slice(args, argIndex, endIndex);
            const extraArgs = _.intersection(maybeArgs, unknownArgs);
            if (extraArgs.length) {
                opts[flag] = [opts[flag]].concat(extraArgs);
                spaceDelimitedArgsMsg(flag, opts[flag]);
                opts[flag] = opts[flag].join(',');
            }
        });
    }
    debug('variable-length opts parsed %o', { args, opts });
    return xvfb.util.parseOpts(opts);
};
const descriptions = {
    autoCancelAfterFailures: 'overrides the project-level Cloud configuration to set the failed test threshold for auto cancellation or to disable auto cancellation when recording to the Cloud',
    browser: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.',
    cacheClear: 'delete all cached binaries',
    cachePrune: 'deletes all cached binaries except for the version currently in use',
    cacheList: 'list cached binary versions',
    cachePath: 'print the path to the binary cache',
    cacheSize: 'Used with the list command to show the sizes of the cached folders',
    ciBuildId: 'the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers',
    component: 'runs component tests',
    config: 'sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs}.',
    configFile: 'path to script file where configuration values are set. defaults to "cypress.config.{js,ts,mjs,cjs}".',
    detached: 'runs Cypress application in detached mode',
    dev: 'runs cypress in development and bypasses binary check',
    e2e: 'runs end to end tests',
    env: 'sets environment variables. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs} or cypress.env.json',
    expose: 'sets exposed public configuration variables. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs}',
    exit: 'keep the browser open after tests finish',
    forceInstall: 'force install the Cypress binary',
    global: 'force Cypress into global mode as if it were globally installed',
    group: 'a named group for recorded runs in Cypress Cloud',
    headed: 'displays the browser instead of running headlessly',
    headless: 'hide the browser instead of running headed (default for cypress run)',
    key: 'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.',
    parallel: 'enables concurrent runs and automatic load balancing of specs across multiple machines or processes',
    passWithNoTests: 'pass when no tests are found',
    port: 'runs Cypress on a specific port. overrides any value in cypress.config.{js,ts,mjs,cjs}.',
    project: 'path to the project',
    posixExitCodes: 'use POSIX exit codes for error handling',
    quiet: 'run quietly, using only the configured reporter',
    record: 'records the run. sends test results, screenshots and videos to Cypress Cloud.',
    reporter: 'runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"',
    reporterOptions: 'options for the mocha reporter. defaults to "null"',
    runnerUi: 'displays the Cypress Runner UI',
    noRunnerUi: 'hides the Cypress Runner UI',
    spec: 'runs specific spec file(s). defaults to "all"',
    tag: 'named tag(s) for recorded runs in Cypress Cloud',
    version: 'prints Cypress version',
};
const knownCommands = [
    'cache',
    'help',
    '-h',
    '--help',
    'install',
    'open',
    'run',
    'verify',
    '-v',
    '--version',
    'version',
    'info',
];
const text = (description) => {
    if (!descriptions[description]) {
        throw new Error(`Could not find description for: ${description}`);
    }
    return descriptions[description];
};
function includesVersion(args) {
    return (_.includes(args, '--version') ||
        _.includes(args, '-v'));
}
function showVersions(opts) {
    return xvfb.__awaiter(this, void 0, void 0, function* () {
        debug('printing Cypress version');
        debug('additional arguments %o', opts);
        debug('parsed version arguments %o', opts);
        const reportAllVersions = (versions) => {
            xvfb.loggerModule.always('Cypress package version:', versions.package);
            xvfb.loggerModule.always('Cypress binary version:', versions.binary);
            xvfb.loggerModule.always('Electron version:', versions.electronVersion);
            xvfb.loggerModule.always('Bundled Node version:', versions.electronNodeVersion);
        };
        const reportComponentVersion = (componentName, versions) => {
            const names = {
                package: 'package',
                binary: 'binary',
                electron: 'electronVersion',
                node: 'electronNodeVersion',
            };
            if (!names[componentName]) {
                throw new Error(`Unknown component name "${componentName}"`);
            }
            const name = names[componentName];
            if (!versions[name]) {
                throw new Error(`Cannot find version for component "${componentName}" under property "${name}"`);
            }
            const version = versions[name];
            xvfb.loggerModule.always(version);
        };
        const defaultVersions = {
            package: undefined,
            binary: undefined,
            electronVersion: undefined,
            electronNodeVersion: undefined,
        };
        try {
            const versions = (yield versionsModule.getVersions()) || defaultVersions;
            if (opts === null || opts === void 0 ? void 0 : opts.component) {
                reportComponentVersion(opts.component, versions);
            }
            else {
                reportAllVersions(versions);
            }
            process.exit(0);
        }
        catch (e) {
            xvfb.util.logErrorExit1(e);
        }
    });
}
const createProgram = () => {
    const program = new commander.Command();
    // bug in commander not printing name
    // in usage help docs
    program._name = 'cypress';
    program.usage('<command> [options]');
    return program;
};
const addCypressRunCommand = (program) => {
    return program
        .command('run')
        .usage('[options]')
        .description('Runs Cypress tests from the CLI without the GUI')
        .option('--auto-cancel-after-failures <test-failure-count || false>', text('autoCancelAfterFailures'))
        .option('-b, --browser <browser-name-or-path>', text('browser'))
        .option('--ci-build-id <id>', text('ciBuildId'))
        .option('--component', text('component'))
        .option('-c, --config <config>', text('config'))
        .option('-C, --config-file <config-file>', text('configFile'))
        .option('--e2e', text('e2e'))
        .option('-e, --env <env>', text('env'))
        .option('-x, --expose <expose>', text('expose'))
        .option('--group <name>', text('group'))
        .option('-k, --key <record-key>', text('key'))
        .option('--headed', text('headed'))
        .option('--headless', text('headless'))
        .option('--no-exit', text('exit'))
        .option('--parallel', text('parallel'))
        .option('--pass-with-no-tests', text('passWithNoTests'))
        .option('-p, --port <port>', text('port'))
        .option('-P, --project <project-path>', text('project'))
        .option('--posix-exit-codes', text('posixExitCodes'))
        .option('-q, --quiet', text('quiet'))
        .option('--record [bool]', text('record'), coerceFalse)
        .option('-r, --reporter <reporter>', text('reporter'))
        .option('--runner-ui', text('runnerUi'))
        .option('--no-runner-ui', text('noRunnerUi'))
        .option('-o, --reporter-options <reporter-options>', text('reporterOptions'))
        .option('-s, --spec <spec>', text('spec'))
        .option('-t, --tag <tag>', text('tag'))
        .option('--dev', text('dev'), coerceFalse);
};
const addCypressOpenCommand = (program) => {
    return program
        .command('open')
        .usage('[options]')
        .description('Opens Cypress in the interactive GUI.')
        .option('-b, --browser <browser-path>', text('browser'))
        .option('--component', text('component'))
        .option('-c, --config <config>', text('config'))
        .option('-C, --config-file <config-file>', text('configFile'))
        .option('-d, --detached [bool]', text('detached'), coerceFalse)
        .option('--e2e', text('e2e'))
        .option('-e, --env <env>', text('env'))
        .option('-x, --expose <expose>', text('expose'))
        .option('--global', text('global'))
        .option('-p, --port <port>', text('port'))
        .option('-P, --project <project-path>', text('project'))
        .option('--dev', text('dev'), coerceFalse);
};
const maybeAddInspectFlags = (program) => {
    if (process.argv.includes('--dev')) {
        return program
            .option('--inspect', 'Node option')
            .option('--inspect-brk', 'Node option');
    }
    return program;
};
/**
 * Casts known command line options for "cypress run" to their intended type.
 * For example if the user passes "--port 5005" the ".port" property should be
 * a number 5005 and not a string "5005".
 *
 * Returns a clone of the original object.
 */
const castCypressOptions = (opts) => {
    // only properties that have type "string | false" in our TS definition
    // require special handling, because CLI parsing takes care of purely
    // boolean arguments
    const castOpts = Object.assign({}, opts);
    if (_.has(opts, 'port')) {
        castOpts.port = coerceAnyStringToInt(opts.port);
    }
    return castOpts;
};
const cliModule = {
    /**
     * Parses `cypress run` command line option array into an object
     * with options that you can feed into a `cypress.run()` module API call.
     * @example
     *  const options = parseRunCommand(['cypress', 'run', '--browser', 'chrome'])
     *  // options is {browser: 'chrome'}
     */
    parseRunCommand(args) {
        return new Promise((resolve, reject) => {
            if (!Array.isArray(args)) {
                return reject(new Error('Expected array of arguments'));
            }
            // make a copy of the input arguments array
            // and add placeholders where "node ..." would usually be
            // also remove "cypress" keyword at the start if present
            const cliArgs = args[0] === 'cypress' ? [...args.slice(1)] : [...args];
            cliArgs.unshift(null, null);
            debug('creating program parser');
            const program = createProgram();
            maybeAddInspectFlags(addCypressRunCommand(program))
                .action((...fnArgs) => {
                debug('parsed Cypress run %o', fnArgs);
                const options = parseVariableOpts(fnArgs, cliArgs);
                debug('parsed options %o', options);
                const casted = castCypressOptions(options);
                debug('casted options %o', casted);
                resolve(casted);
            });
            debug('parsing args: %o', cliArgs);
            program.parse(cliArgs);
        });
    },
    /**
     * Parses `cypress open` command line option array into an object
     * with options that you can feed into cy.openModeSystemTest test calls
     * @example
     *  const options = parseOpenCommand(['cypress', 'open', '--browser', 'chrome'])
     *  // options is {browser: 'chrome'}
     */
    parseOpenCommand(args) {
        return new Promise((resolve, reject) => {
            if (!Array.isArray(args)) {
                return reject(new Error('Expected array of arguments'));
            }
            // make a copy of the input arguments array
            // and add placeholders where "node ..." would usually be
            // also remove "cypress" keyword at the start if present
            const cliArgs = args[0] === 'cypress' ? [...args.slice(1)] : [...args];
            cliArgs.unshift(null, null);
            debug('creating program parser');
            const program = createProgram();
            maybeAddInspectFlags(addCypressOpenCommand(program))
                .action((...fnArgs) => {
                debug('parsed Cypress open %o', fnArgs);
                const options = parseVariableOpts(fnArgs, cliArgs);
                debug('parsed options %o', options);
                const casted = castCypressOptions(options);
                debug('casted options %o', casted);
                resolve(casted);
            });
            debug('parsing args: %o', cliArgs);
            program.parse(cliArgs);
        });
    },
    /**
     * Parses the command line and kicks off Cypress process.
     */
    init(args) {
        return xvfb.__awaiter(this, void 0, void 0, function* () {
            if (!args) {
                args = process.argv;
            }
            const { CYPRESS_INTERNAL_ENV, CYPRESS_DOWNLOAD_USE_CA } = process.env;
            if (process.env.CYPRESS_DOWNLOAD_USE_CA) {
                let msg = `
        ${logSymbols.warning} Warning: It looks like you're setting CYPRESS_DOWNLOAD_USE_CA=${CYPRESS_DOWNLOAD_USE_CA}

        The environment variable "CYPRESS_DOWNLOAD_USE_CA" is no longer required to be set.
        
        You can safely unset this environment variable.
      `;
                xvfb.loggerModule.log();
                xvfb.loggerModule.warn(commonTags.stripIndent(msg));
                xvfb.loggerModule.log();
            }
            if (!xvfb.util.isValidCypressInternalEnvValue(CYPRESS_INTERNAL_ENV)) {
                debug('invalid CYPRESS_INTERNAL_ENV value', CYPRESS_INTERNAL_ENV);
                return xvfb.exitWithError(xvfb.errors.invalidCypressEnv)(`CYPRESS_INTERNAL_ENV=${CYPRESS_INTERNAL_ENV}`);
            }
            if (xvfb.util.isNonProductionCypressInternalEnvValue(CYPRESS_INTERNAL_ENV)) {
                debug('non-production CYPRESS_INTERNAL_ENV value', CYPRESS_INTERNAL_ENV);
                let msg = `
        ${logSymbols.warning} Warning: It looks like you're passing CYPRESS_INTERNAL_ENV=${CYPRESS_INTERNAL_ENV}

        The environment variable "CYPRESS_INTERNAL_ENV" is reserved and should only be used internally.

        Unset the "CYPRESS_INTERNAL_ENV" environment variable and run Cypress again.
      `;
                xvfb.loggerModule.log();
                xvfb.loggerModule.warn(commonTags.stripIndent(msg));
                xvfb.loggerModule.log();
            }
            const program = createProgram();
            program
                .command('help')
                .description('Shows CLI help and exits')
                .action(() => {
                program.help();
            });
            const handleVersion = (cmd) => {
                return cmd
                    .option('--component <package|binary|electron|node>', 'component to report version for')
                    .action((opts, ...other) => {
                    showVersions(xvfb.util.parseOpts(opts));
                });
            };
            handleVersion(program
                .storeOptionsAsProperties()
                .option('-v, --version', text('version'))
                .command('version')
                .description(text('version')));
            maybeAddInspectFlags(addCypressOpenCommand(program))
                .action((opts) => xvfb.__awaiter(this, void 0, void 0, function* () {
                debug('opening Cypress');
                try {
                    const code = yield openModule.start(xvfb.util.parseOpts(opts));
                    process.exit(code);
                }
                catch (e) {
                    xvfb.util.logErrorExit1(e);
                }
            }));
            maybeAddInspectFlags(addCypressRunCommand(program))
                .action((...fnArgs) => xvfb.__awaiter(this, void 0, void 0, function* () {
                debug('running Cypress with args %o', fnArgs);
                try {
                    const code = yield runModule.start(parseVariableOpts(fnArgs, args));
                    process.exit(code);
                }
                catch (e) {
                    xvfb.util.logErrorExit1(e);
                }
            }));
            program
                .command('install')
                .usage('[options]')
                .description('Installs the Cypress executable matching this package\'s version')
                .option('-f, --force', text('forceInstall'))
                .action((opts) => xvfb.__awaiter(this, void 0, void 0, function* () {
                try {
                    yield installModule.start(xvfb.util.parseOpts(opts));
                }
                catch (e) {
                    xvfb.util.logErrorExit1(e);
                }
            }));
            program
                .command('verify')
                .usage('[options]')
                .description('Verifies that Cypress is installed correctly and executable')
                .option('--dev', text('dev'), coerceFalse)
                .action((opts) => xvfb.__awaiter(this, void 0, void 0, function* () {
                const defaultOpts = { force: true, welcomeMessage: false };
                const parsedOpts = xvfb.util.parseOpts(opts);
                const options = _.extend(parsedOpts, defaultOpts);
                try {
                    yield spawn.start(options);
                }
                catch (e) {
                    xvfb.util.logErrorExit1(e);
                }
            }));
            program
                .command('cache')
                .usage('[command]')
                .description('Manages the Cypress binary cache')
                .option('list', text('cacheList'))
                .option('path', text('cachePath'))
                .option('clear', text('cacheClear'))
                .option('prune', text('cachePrune'))
                .option('--size', text('cacheSize'))
                .action(function (opts, args) {
                return xvfb.__awaiter(this, void 0, void 0, function* () {
                    if (!args || !args.length) {
                        this.outputHelp();
                        process.exit(1);
                    }
                    const [command] = args;
                    if (!_.includes(['list', 'path', 'clear', 'prune'], command)) {
                        unknownOption.call(this, `cache ${command}`, 'command');
                    }
                    if (command === 'list') {
                        debug('cache command %o', {
                            command,
                            size: opts.size,
                        });
                        try {
                            const result = yield cacheModule.list(opts.size);
                            return result;
                        }
                        catch (e) {
                            if (e.code === 'ENOENT') {
                                xvfb.loggerModule.always('No cached binary versions were found.');
                                process.exit(0);
                            }
                            xvfb.util.logErrorExit1(e);
                        }
                    }
                    cacheModule[command]();
                });
            });
            program
                .command('info')
                .usage('[command]')
                .description('Prints Cypress and system information')
                .option('--dev', text('dev'), coerceFalse)
                .action((opts) => xvfb.__awaiter(this, void 0, void 0, function* () {
                try {
                    const code = yield methods.start(opts);
                    process.exit(code);
                }
                catch (e) {
                    xvfb.util.logErrorExit1(e);
                }
            }));
            debug('cli starts with arguments %j', args);
            xvfb.util.printNodeOptions();
            // if there are no arguments
            if (args.length <= 2) {
                debug('printing help');
                program.help();
                // exits
            }
            const firstCommand = args[2];
            if (!_.includes(knownCommands, firstCommand)) {
                debug('unknown command %s', firstCommand);
                xvfb.loggerModule.error('Unknown command', `"${firstCommand}"`);
                program.outputHelp();
                return process.exit(1);
            }
            if (includesVersion(args)) {
                // commander 2.11.0 changes behavior
                // and now does not understand top level options
                // .option('-v, --version').command('version')
                // so we have to manually catch '-v, --version'
                handleVersion(program);
            }
            debug('program parsing arguments');
            return program.parse(args);
        });
    },
};

exports.cliModule = cliModule;
exports.installModule = installModule;
exports.openModule = openModule;
exports.runModule = runModule;
