"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = __importDefault(require("lodash"));
const assert_1 = __importDefault(require("assert"));
const arch_1 = __importDefault(require("arch"));
const os_1 = __importDefault(require("os"));
const ospath_1 = __importDefault(require("ospath"));
const hasha_1 = __importDefault(require("hasha"));
const tty_1 = __importDefault(require("tty"));
const path_1 = __importDefault(require("path"));
const ci_info_1 = require("ci-info");
const execa_1 = __importDefault(require("execa"));
const systeminformation_1 = __importDefault(require("systeminformation"));
const chalk_1 = __importDefault(require("chalk"));
const bluebird_1 = __importDefault(require("bluebird"));
const cachedir_1 = __importDefault(require("cachedir"));
const log_symbols_1 = __importDefault(require("log-symbols"));
const executable_1 = __importDefault(require("executable"));
const process_1 = require("process");
const common_tags_1 = require("common-tags");
const supports_color_1 = __importDefault(require("supports-color"));
const is_installed_globally_1 = __importDefault(require("is-installed-globally"));
const logger_1 = __importDefault(require("./logger"));
const debug_1 = __importDefault(require("debug"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const package_json_1 = __importDefault(require("../package.json"));
const debug = (0, debug_1.default)('cypress:cli');
const issuesUrl = 'https://github.com/cypress-io/cypress/issues';
/**
 * Returns SHA512 of a file
 */
const getFileChecksum = (filename) => {
    assert_1.default.ok(lodash_1.default.isString(filename) && !lodash_1.default.isEmpty(filename), 'expected filename');
    return hasha_1.default.fromFile(filename, { algorithm: 'sha512' });
};
const getFileSize = (filename) => __awaiter(void 0, void 0, void 0, function* () {
    assert_1.default.ok(lodash_1.default.isString(filename) && !lodash_1.default.isEmpty(filename), 'expected filename');
    const { size } = yield fs_extra_1.default.stat(filename);
    return size;
});
const isBrokenGtkDisplayRe = /Gtk: cannot open display/;
const stringify = (val) => {
    return lodash_1.default.isObject(val) ? JSON.stringify(val) : val;
};
function normalizeModuleOptions(options = {}) {
    return lodash_1.default.mapValues(options, stringify);
}
/**
 * Returns true if the platform is Linux. We do a lot of different
 * stuff on Linux (like Xvfb) and it helps to has readable code
 */
const isLinux = () => {
    return os_1.default.platform() === 'linux';
};
/**
   * If the DISPLAY variable is set incorrectly, when trying to spawn
   * Cypress executable we get an error like this:
  ```
  [1005:0509/184205.663837:WARNING:browser_main_loop.cc(258)] Gtk: cannot open display: 99
  ```
   */
const isBrokenGtkDisplay = (str) => {
    return isBrokenGtkDisplayRe.test(str);
};
const isPossibleLinuxWithIncorrectDisplay = () => {
    return isLinux() && !!process.env.DISPLAY;
};
const logBrokenGtkDisplayWarning = () => {
    debug('Cypress exited due to a broken gtk display because of a potential invalid DISPLAY env... retrying after starting Xvfb');
    // if we get this error, we are on Linux and DISPLAY is set
    logger_1.default.warn((0, common_tags_1.stripIndent) `

    ${log_symbols_1.default.warning} Warning: Cypress failed to start.

    This is likely due to a misconfigured DISPLAY environment variable.

    DISPLAY was set to: "${process.env.DISPLAY}"

    Cypress will attempt to fix the problem and rerun.
  `);
    logger_1.default.warn();
};
function stdoutLineMatches(expectedLine, stdout) {
    const lines = stdout.split('\n').map((val) => val.trim());
    return lines.some((line) => line === expectedLine);
}
/**
 * Confirms if given value is a valid CYPRESS_INTERNAL_ENV value. Undefined values
 * are valid, because the system can set the default one.
 *
 * @param {string} value
 * @example util.isValidCypressInternalEnvValue(process.env.CYPRESS_INTERNAL_ENV)
 */
function isValidCypressInternalEnvValue(value) {
    if (lodash_1.default.isUndefined(value)) {
        // will get default value
        return true;
    }
    // names of config environments, see "packages/server/config/app.json"
    const names = ['development', 'test', 'staging', 'production'];
    return lodash_1.default.includes(names, value);
}
/**
 * Confirms if given value is a non-production CYPRESS_INTERNAL_ENV value.
 * Undefined values are valid, because the system can set the default one.
 *
 * @param {string} value
 * @example util.isNonProductionCypressInternalEnvValue(process.env.CYPRESS_INTERNAL_ENV)
 */
function isNonProductionCypressInternalEnvValue(value) {
    return !lodash_1.default.isUndefined(value) && value !== 'production';
}
/**
 * Prints NODE_OPTIONS using debug() module, but only
 * if DEBUG=cypress... is set
 */
function printNodeOptions(log = debug) {
    if (!log.enabled) {
        return;
    }
    if (process.env.NODE_OPTIONS) {
        log('NODE_OPTIONS=%s', process.env.NODE_OPTIONS);
    }
    else {
        log('NODE_OPTIONS is not set');
    }
}
/**
 * Removes double quote characters
 * from the start and end of the given string IF they are both present
 *
 * @param {string} str Input string
 * @returns {string} Trimmed string or the original string if there are no double quotes around it.
 * @example
  ```
  dequote('"foo"')
  // returns string 'foo'
  dequote('foo')
  // returns string 'foo'
  ```
 */
const dequote = (str) => {
    assert_1.default.ok(lodash_1.default.isString(str), 'expected a string to remove double quotes');
    if (str.length > 1 && str[0] === '"' && str[str.length - 1] === '"') {
        return str.substr(1, str.length - 2);
    }
    return str;
};
const parseOpts = (opts) => {
    opts = lodash_1.default.pick(opts, 'autoCancelAfterFailures', 'browser', 'cachePath', 'cacheList', 'cacheClear', 'cachePrune', 'ciBuildId', 'ct', 'component', 'config', 'configFile', 'cypressVersion', 'destination', 'detached', 'dev', 'e2e', 'exit', 'env', 'force', 'global', 'group', 'headed', 'headless', 'inspect', 'inspectBrk', 'key', 'path', 'parallel', 'port', 'posixExitCodes', 'project', 'quiet', 'reporter', 'reporterOptions', 'record', 'runnerUi', 'runProject', 'spec', 'tag');
    if (opts.exit) {
        opts = lodash_1.default.omit(opts, 'exit');
    }
    // some options might be quoted - which leads to unexpected results
    // remove double quotes from certain options
    const cleanOpts = Object.assign({}, opts);
    const toDequote = ['group', 'ciBuildId'];
    for (const prop of toDequote) {
        if (lodash_1.default.has(opts, prop)) {
            cleanOpts[prop] = dequote(opts[prop]);
        }
    }
    debug('parsed cli options %o', cleanOpts);
    return cleanOpts;
};
/**
 * Copy of packages/server/lib/browsers/utils.ts
 * because we need same functionality in CLI to show the path :(
 */
const getApplicationDataFolder = (...paths) => {
    const { env } = process;
    // allow overriding the app_data folder
    let folder = env.CYPRESS_CONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development';
    // @ts-expect-error value exists but is not typed
    const PRODUCT_NAME = package_json_1.default.productName || package_json_1.default.name;
    const OS_DATA_PATH = ospath_1.default.data();
    const ELECTRON_APP_DATA_PATH = path_1.default.join(OS_DATA_PATH, PRODUCT_NAME);
    if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
        folder = `${folder}-e2e-test`;
    }
    const p = path_1.default.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths);
    return p;
};
const util = {
    normalizeModuleOptions,
    parseOpts,
    isValidCypressInternalEnvValue,
    isNonProductionCypressInternalEnvValue,
    printNodeOptions,
    isCi() {
        return ci_info_1.isCI;
    },
    getEnvOverrides(options = {}) {
        return lodash_1.default
            .chain({})
            .extend(this.getEnvColors())
            .extend(this.getForceTty())
            .omitBy(lodash_1.default.isUndefined) // remove undefined values
            .mapValues((value) => {
            return value ? '1' : '0';
        })
            .extend(this.getOriginalNodeOptions())
            .value();
    },
    getOriginalNodeOptions() {
        const opts = {};
        if (process.env.NODE_OPTIONS) {
            opts.ORIGINAL_NODE_OPTIONS = process.env.NODE_OPTIONS;
        }
        return opts;
    },
    getForceTty() {
        return {
            FORCE_STDIN_TTY: this.isTty(process.stdin.fd),
            FORCE_STDOUT_TTY: this.isTty(process.stdout.fd),
            FORCE_STDERR_TTY: this.isTty(process.stderr.fd),
        };
    },
    getEnvColors() {
        const sc = this.supportsColor();
        return {
            FORCE_COLOR: sc,
            DEBUG_COLORS: sc,
            MOCHA_COLORS: sc ? true : undefined,
        };
    },
    isTty(fd) {
        return tty_1.default.isatty(fd);
    },
    supportsColor() {
        // if we've been explicitly told not to support
        // color then turn this off
        if (process.env.NO_COLOR) {
            return false;
        }
        // https://github.com/cypress-io/cypress/issues/1747
        // always return true in CI providers
        if (process.env.CI) {
            return true;
        }
        // ensure that both stdout and stderr support color
        return Boolean(supports_color_1.default.stdout) && Boolean(supports_color_1.default.stderr);
    },
    cwd() {
        return (0, process_1.cwd)();
    },
    pkgBuildInfo() {
        // @ts-expect-error value exists but is not typed
        return package_json_1.default.buildInfo;
    },
    pkgVersion() {
        return package_json_1.default.version;
    },
    // TODO: remove this method
    exit(code) {
        process.exit(code);
    },
    logErrorExit1(err) {
        logger_1.default.error(err.message);
        process.exit(1);
    },
    dequote,
    titleize(...args) {
        // prepend first arg with space
        // and pad so that all messages line up
        args[0] = lodash_1.default.padEnd(` ${args[0]}`, 24);
        // get rid of any falsy values
        args = lodash_1.default.compact(args);
        return chalk_1.default.blue(...args);
    },
    calculateEta(percent, elapsed) {
        // returns the number of seconds remaining
        // if we're at 100% already just return 0
        if (percent === 100) {
            return 0;
        }
        // take the percentage and divide by one
        // and multiple that against elapsed
        // subtracting what's already elapsed
        return elapsed * (1 / (percent / 100)) - elapsed;
    },
    convertPercentToPercentage(num) {
        // convert a percent with values between 0 and 1
        // with decimals, so that it is between 0 and 100
        // and has no decimal places
        return Math.round(lodash_1.default.isFinite(num) ? (num * 100) : 0);
    },
    secsRemaining(eta) {
        // calculate the seconds reminaing with no decimal places
        return (lodash_1.default.isFinite(eta) ? (eta / 1000) : 0).toFixed(0);
    },
    setTaskTitle(task, title, renderer) {
        // only update the renderer title when not running in CI
        if (renderer === 'default' && task.title !== title) {
            task.title = title;
        }
    },
    isInstalledGlobally() {
        return is_installed_globally_1.default;
    },
    isSemver(str) {
        return /^(\d+\.)?(\d+\.)?(\*|\d+)$/.test(str);
    },
    isExecutableAsync(filePath) {
        return bluebird_1.default.resolve((0, executable_1.default)(filePath));
    },
    isLinux,
    getOsVersionAsync() {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const osInfo = yield systeminformation_1.default.osInfo();
                if (osInfo.distro && osInfo.release) {
                    return `${osInfo.distro} - ${osInfo.release}`;
                }
            }
            catch (err) {
                return os_1.default.release();
            }
            return os_1.default.release();
        });
    },
    getPlatformInfo() {
        return __awaiter(this, void 0, void 0, function* () {
            const [version, osArch] = yield bluebird_1.default.all([
                this.getOsVersionAsync(),
                this.getRealArch(),
            ]);
            return (0, common_tags_1.stripIndent) `
      Platform: ${os_1.default.platform()}-${osArch} (${version})
      Cypress Version: ${this.pkgVersion()}
    `;
        });
    },
    _cachedArch: undefined,
    /**
     * Attempt to return the real system arch (not process.arch, which is only the Node binary's arch)
     */
    getRealArch() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this._cachedArch)
                return this._cachedArch;
            function _getRealArch() {
                return __awaiter(this, void 0, void 0, function* () {
                    const osPlatform = os_1.default.platform();
                    const osArch = os_1.default.arch();
                    debug('detecting arch %o', { osPlatform, osArch });
                    if (osArch === 'arm64')
                        return 'arm64';
                    if (osPlatform === 'darwin') {
                        // could possibly be x64 node on arm64 darwin, check if we are being translated by Rosetta
                        // https://stackoverflow.com/a/65347893/3474615
                        const { stdout } = yield (0, execa_1.default)('sysctl', ['-n', 'sysctl.proc_translated']).catch(() => ({ stdout: '' }));
                        debug('rosetta check result: %o', { stdout });
                        if (stdout === '1')
                            return 'arm64';
                    }
                    if (osPlatform === 'linux') {
                        // could possibly be x64 node on arm64 linux, check the "machine hardware name"
                        // list of names for reference: https://stackoverflow.com/a/45125525/3474615
                        const { stdout } = yield (0, execa_1.default)('uname', ['-m']).catch(() => ({ stdout: '' }));
                        debug('arm uname -m result: %o ', { stdout });
                        if (['aarch64_be', 'aarch64', 'armv8b', 'armv8l'].includes(stdout))
                            return 'arm64';
                    }
                    const pkgArch = (0, arch_1.default)();
                    if (pkgArch === 'x86')
                        return 'ia32';
                    return pkgArch;
                });
            }
            return (this._cachedArch = yield _getRealArch());
        });
    },
    // attention:
    // when passing relative path to NPM post install hook, the current working
    // directory is set to the `node_modules/cypress` folder
    // the user is probably passing relative path with respect to root package folder
    formAbsolutePath(filename) {
        if (path_1.default.isAbsolute(filename)) {
            return filename;
        }
        return path_1.default.join((0, process_1.cwd)(), '..', '..', filename);
    },
    getEnv(varName, trim) {
        assert_1.default.ok(lodash_1.default.isString(varName) && !lodash_1.default.isEmpty(varName), 'expected environment variable name, not');
        const configVarName = `npm_config_${varName}`;
        const configVarNameLower = configVarName.toLowerCase();
        const packageConfigVarName = `npm_package_config_${varName}`;
        let result;
        if (process.env.hasOwnProperty(varName)) {
            debug(`Using ${varName} from environment variable`);
            result = process.env[varName];
        }
        else if (process.env.hasOwnProperty(configVarName)) {
            debug(`Using ${varName} from npm config`);
            result = process.env[configVarName];
        }
        else if (process.env.hasOwnProperty(configVarNameLower)) {
            debug(`Using ${varName.toLowerCase()} from npm config`);
            result = process.env[configVarNameLower];
        }
        else if (process.env.hasOwnProperty(packageConfigVarName)) {
            debug(`Using ${varName} from package.json config`);
            result = process.env[packageConfigVarName];
        }
        // environment variables are often set double quotes to escape characters
        // and on Windows it can lead to weird things: for example
        //  set FOO="C:\foo.txt" && node -e "console.log('>>>%s<<<', process.env.FOO)"
        // will print
        //    >>>"C:\foo.txt" <<<
        // see https://github.com/cypress-io/cypress/issues/4506#issuecomment-506029942
        // so for sanity sake we should first trim whitespace characters and remove
        // double quotes around environment strings if the caller is expected to
        // use this environment string as a file path
        return trim && (result !== null && result !== undefined) ? dequote(lodash_1.default.trim(result)) : result;
    },
    getCacheDir() {
        return (0, cachedir_1.default)('Cypress');
    },
    isPostInstall() {
        return process.env.npm_lifecycle_event === 'postinstall';
    },
    exec: execa_1.default,
    stdoutLineMatches,
    issuesUrl,
    isBrokenGtkDisplay,
    logBrokenGtkDisplayWarning,
    isPossibleLinuxWithIncorrectDisplay,
    getGitHubIssueUrl(number) {
        assert_1.default.ok(lodash_1.default.isInteger(number), 'github issue should be an integer');
        assert_1.default.ok(number > 0, 'github issue should be a positive number');
        return `${issuesUrl}/${number}`;
    },
    getFileChecksum,
    getFileSize,
    getApplicationDataFolder,
};
exports.default = util;
