"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.run = void 0;
const isString_1 = __importDefault(require("lodash/isString"));
const path_1 = __importDefault(require("path"));
const prompts_ncu_1 = __importDefault(require("prompts-ncu"));
const spawn_please_1 = __importDefault(require("spawn-please"));
const cli_options_1 = require("./cli-options");
const cache_1 = require("./lib/cache");
const chalk_1 = __importStar(require("./lib/chalk"));
const determinePackageManager_1 = __importDefault(require("./lib/determinePackageManager"));
const doctor_1 = __importDefault(require("./lib/doctor"));
const exists_1 = __importDefault(require("./lib/exists"));
const findPackage_1 = __importDefault(require("./lib/findPackage"));
const getAllPackages_1 = __importDefault(require("./lib/getAllPackages"));
const getNcuRc_1 = __importDefault(require("./lib/getNcuRc"));
const initOptions_1 = __importDefault(require("./lib/initOptions"));
const logging_1 = require("./lib/logging");
const mergeOptions_1 = __importDefault(require("./lib/mergeOptions"));
const programError_1 = __importDefault(require("./lib/programError"));
const runGlobal_1 = __importDefault(require("./lib/runGlobal"));
const runLocal_1 = __importDefault(require("./lib/runLocal"));
// allow prompt injection from environment variable for testing purposes
if (process.env.INJECT_PROMPTS) {
    prompts_ncu_1.default.inject(JSON.parse(process.env.INJECT_PROMPTS));
}
// Exit with non-zero error code when there is an unhandled promise rejection.
// Use `node --trace-uncaught ...` to show where the exception was thrown.
// See: https://nodejs.org/api/process.html#event-unhandledrejection
process.on('unhandledRejection', (reason) => {
    // do not rethrow, as there may be other errors to print out
    console.error(reason);
});
/**
 * Volta is a tool for managing JavaScript tooling like Node and npm. Volta has
 * its own system for installing global packages which circumvents npm, so
 * commands like `npm ls -g` do not accurately reflect what is installed.
 *
 * The ability to use `npm ls -g` is tracked in this Volta issue: https://github.com/volta-cli/volta/issues/1012
 */
const noVolta = (options) => {
    var _a;
    // The first check is for macOS/Linux and the second check is for Windows
    if (options.global && (!!process.env.VOLTA_HOME || ((_a = process.env.PATH) === null || _a === void 0 ? void 0 : _a.includes('\\Volta')))) {
        const message = 'It appears you are using Volta. `npm-check-updates --global` ' +
            'cannot be used with Volta because Volta has its own system for ' +
            'managing global packages which circumvents npm.\n\n' +
            'If you are still receiving this message after uninstalling Volta, ' +
            'ensure your PATH does not contain an entry for Volta and your ' +
            'shell profile does not define VOLTA_HOME. You may need to reboot ' +
            'for changes to your shell profile to take effect.';
        (0, logging_1.print)(options, message, 'error');
        process.exit(1);
    }
};
/** Returns the package manager that should be used to install packages after running "ncu -u". Detects pnpm via pnpm-lock.yaml. This is the one place that pnpm needs to be detected, since otherwise it is backwards compatible with npm. */
const getPackageManagerForInstall = async (options, pkgFile) => {
    var _a;
    // when packageManager is set to staticRegistry, we need to infer the package manager from lock files
    if (options.packageManager === 'staticRegistry')
        (0, determinePackageManager_1.default)({ ...options, packageManager: undefined });
    else if (options.packageManager !== 'npm')
        return options.packageManager;
    const cwd = ((_a = options.cwd) !== null && _a !== void 0 ? _a : pkgFile) ? `${pkgFile}/..` : process.cwd();
    const pnpmDetected = await (0, exists_1.default)(path_1.default.join(cwd, 'pnpm-lock.yaml'));
    return pnpmDetected ? 'pnpm' : 'npm';
};
/** Returns if analysis contains upgrades */
const someUpgraded = (pkgs, analysis) => {
    // deep mode analysis is of type Index<PackageFile>
    // non-deep mode analysis is of type <PackageFile>, so we normalize it to Index<PackageFile>
    const analysisNormalized = pkgs.length === 1 ? { [pkgs[0]]: analysis } : analysis;
    return Object.values(analysisNormalized).some(upgrades => Object.keys(upgrades).length > 0);
};
/** Either suggest an install command based on the package manager, or in interactive mode, prompt to auto-install. */
const install = async (pkgs, analysis, options) => {
    var _a;
    if (options.install === 'never') {
        (0, logging_1.print)(options, '');
        return;
    }
    // if no packages were upgraded (i.e. all dependencies deselected in interactive mode), then bail without suggesting an install.
    // normalize the analysis for one or many packages
    if (!someUpgraded(pkgs, analysis))
        return;
    // for the purpose of the install hint, just use the package manager used in the first sub-project
    // if auto-installing, the actual package manager in each sub-project will be used
    const packageManager = await getPackageManagerForInstall(options, pkgs[0]);
    // by default, show an install hint after upgrading
    // this will be disabled in interactive mode if the user chooses to have npm-check-updates execute the install command
    const installHint = `Run ${chalk_1.default.cyan(packageManager + ' install')}${pkgs.length > 1 && !options.workspace && !options.workspaces ? ' in each project directory' : ''} to install new versions`;
    const isInteractive = options.interactive && !process.env.NCU_DOCTOR;
    // prompt the user if they want ncu to run "npm install"
    let response;
    if (isInteractive && options.install === 'prompt') {
        (0, logging_1.print)(options, '');
        response = await (0, prompts_ncu_1.default)({
            type: 'confirm',
            name: 'value',
            message: `${installHint}?`,
            initial: true,
            // allow Ctrl+C to kill the process
            onState: (state) => {
                if (state.aborted) {
                    process.nextTick(() => process.exit(1));
                }
            },
        });
    }
    // auto-install
    if (options.install === 'always' || (isInteractive && response.value)) {
        if (options.install === 'always') {
            (0, logging_1.print)(options, '');
        }
        (0, logging_1.print)(options, 'Installing dependencies...');
        // only run npm install once in the root when in workspace mode
        // npm install will install packages for all workspaces
        const isWorkspace = options.workspaces || !!((_a = options.workspace) === null || _a === void 0 ? void 0 : _a.length);
        const pkgsNormalized = isWorkspace ? ['package.json'] : pkgs;
        pkgsNormalized.forEach(async (pkgFile) => {
            const packageManager = await getPackageManagerForInstall(options, pkgFile);
            const cmd = packageManager + (process.platform === 'win32' ? '.cmd' : '');
            const cwd = options.cwd || path_1.default.resolve(pkgFile, '..');
            let stdout = '';
            try {
                await (0, spawn_please_1.default)(cmd, ['install'], {
                    cwd,
                    env: {
                        ...process.env,
                        ...(options.color !== false ? { FORCE_COLOR: true } : null),
                        // With spawn, pnpm install will fail with ERR_PNPM_PEER_DEP_ISSUES  Unmet peer dependencies.
                        // When pnpm install is run directly from the terminal, this error does not occur.
                        // When pnpm install is run from a simple spawn script, this error does not occur.
                        // The issue only seems to be when pnpm install is executed from npm-check-updates, but it's not clear what configuration or environmental factors are causing this.
                        // For now, turn off strict-peer-dependencies on pnpm auto-install.
                        // See: https://github.com/raineorshine/npm-check-updates/issues/1191
                        ...(packageManager === 'pnpm' ? { npm_config_strict_peer_dependencies: false } : null),
                    },
                    stdout: (data) => {
                        stdout += data;
                    },
                    stderr: (data) => {
                        console.error(chalk_1.default.red(data.toString()));
                    },
                });
                (0, logging_1.print)(options, stdout);
                (0, logging_1.print)(options, 'Done');
            }
            catch (err) {
                // sometimes packages print errors to stdout instead of stderr
                // if there is nothing on stderr, reject with stdout
                throw new Error((err === null || err === void 0 ? void 0 : err.message) || err || stdout);
            }
        });
    }
    // show the install hint unless auto-install occurred
    else if (!isInteractive) {
        (0, logging_1.print)(options, `\n${installHint}.`);
    }
};
/** Runs the dependency upgrades. Loads the ncurc, finds the package file, and handles --deep. */
async function runUpgrades(options, timeout) {
    var _a;
    const [selectedPackageInfos, workspacePackages] = await (0, getAllPackages_1.default)(options);
    const packageFilepaths = selectedPackageInfos.map((packageInfo) => packageInfo.filepath);
    // enable deep mode if --deep, --workspace, --workspaces, or if multiple package files are found
    const isWorkspace = options.workspaces || !!((_a = options.workspace) === null || _a === void 0 ? void 0 : _a.length);
    options.deep = options.deep || isWorkspace || selectedPackageInfos.length > 1;
    let analysis;
    if (options.global) {
        const analysis = await (0, runGlobal_1.default)(options);
        clearTimeout(timeout);
        return analysis;
    }
    else if (options.deep) {
        analysis = await selectedPackageInfos.reduce(async (previousPromise, packageInfo) => {
            const packages = await previousPromise;
            // copy object to prevent share .ncurc options between different packageFile, to prevent unpredictable behavior
            const rcResult = await (0, getNcuRc_1.default)({ packageFile: packageInfo.filepath, color: options.color });
            let rcConfig = rcResult && rcResult.config ? rcResult.config : {};
            if (options.mergeConfig && Object.keys(rcConfig).length) {
                // Merge config options.
                rcConfig = (0, mergeOptions_1.default)(options, rcConfig);
            }
            const pkgOptions = {
                ...options,
                ...rcConfig,
                packageFile: packageInfo.filepath,
                workspacePackages,
            };
            const { pkgData, pkgFile } = await (0, findPackage_1.default)(pkgOptions);
            return {
                ...packages,
                // index by relative path if cwd was specified
                [pkgOptions.cwd
                    ? path_1.default
                        .relative(path_1.default.resolve(pkgOptions.cwd), pkgFile)
                        // convert Windows path to *nix path for consistency
                        .replace(/\\/g, '/')
                    : pkgFile]: await (0, runLocal_1.default)(pkgOptions, pkgData, pkgFile),
            };
        }, Promise.resolve({}));
        if (options.json) {
            (0, logging_1.printJson)(options, analysis);
        }
    }
    else {
        // mutate packageFile when glob pattern finds only single package
        if (selectedPackageInfos.length === 1 &&
            selectedPackageInfos[0].filepath !== (options.packageFile || 'package.json')) {
            options.packageFile = selectedPackageInfos[0].filepath;
        }
        const { pkgData, pkgFile } = await (0, findPackage_1.default)(options);
        analysis = await (0, runLocal_1.default)(options, pkgData, pkgFile);
    }
    clearTimeout(timeout);
    if (options.errorLevel === 2 && someUpgraded(packageFilepaths, analysis)) {
        (0, programError_1.default)(options, '\nDependencies not up-to-date');
    }
    // suggest install command or auto-install
    if (options.upgrade) {
        // deno does not have an install command
        // The closest equivalent is deno cache, but it is optional.
        // See: https://deno.land/manual@v1.30.3/references/cheatsheet#nodejs---deno-cheatsheet
        if (options.packageManager === 'deno') {
            (0, logging_1.print)(options, '');
        }
        else {
            await install(packageFilepaths, analysis, options);
        }
    }
    return analysis;
}
/** Main entry point.
 *
 * @returns Promise<
 * PackageFile                    Default returns upgraded package file.
 * | Index<VersionSpec>    --jsonUpgraded returns only upgraded dependencies.
 * | void                         --global upgrade returns void.
 * >
 */
async function run(runOptions = {}, { cli } = {}) {
    const options = await (0, initOptions_1.default)(runOptions, { cli });
    // chalk may already have been initialized in cli.ts, but when imported as a module
    // chalkInit is idempotent
    await (0, chalk_1.chalkInit)(options.color);
    noVolta(options);
    (0, logging_1.print)(options, 'Initializing', 'verbose');
    if (options.cacheClear) {
        await (0, cache_1.cacheClear)(options);
    }
    let timeout;
    let timeoutPromise = new Promise(() => null);
    if (options.timeout) {
        const timeoutMs = (0, isString_1.default)(options.timeout) ? Number.parseInt(options.timeout, 10) : options.timeout;
        timeoutPromise = new Promise((resolve, reject) => {
            timeout = setTimeout(() => {
                // must catch the error and reject explicitly since we are in a setTimeout
                const error = `Exceeded global timeout of ${timeoutMs}ms`;
                reject(error);
                try {
                    (0, programError_1.default)(options, error);
                }
                catch (e) {
                    /* noop */
                }
            }, timeoutMs);
        });
    }
    // doctor mode
    if (options.doctor) {
        // execute with -u
        if (options.upgrade) {
            // we have to pass run directly since it would be a circular require if doctor included this file
            return Promise.race([timeoutPromise, (0, doctor_1.default)(run, options)]);
        }
        // print help otherwise
        else {
            const help = typeof cli_options_1.cliOptionsMap.doctor.help === 'function' ? cli_options_1.cliOptionsMap.doctor.help({}) : cli_options_1.cliOptionsMap.doctor.help;
            (0, logging_1.print)(options, `Usage: ncu --doctor\n\n${help}`, 'warn');
        }
    }
    // normal mode
    else {
        return Promise.race([timeoutPromise, runUpgrades(options, timeout)]);
    }
}
exports.run = run;
exports.default = run;
//# sourceMappingURL=index.js.map