// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { AlreadyReportedError, EnvironmentMap, FileConstants, FileSystem, JsonFile, Objects } from '@rushstack/node-core-library';
import { Colorize, ConsoleTerminalProvider, Terminal, PrintUtilities } from '@rushstack/terminal';
import { RushConfiguration } from '../api/RushConfiguration';
import { NodeJsCompatibility } from '../logic/NodeJsCompatibility';
import { RushConstants } from '../logic/RushConstants';
import { RushGlobalFolder } from '../api/RushGlobalFolder';
import { PurgeManager } from '../logic/PurgeManager';
import { Utilities } from '../utilities/Utilities';
import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration';
import { initializeDotEnv } from '../logic/dotenv';
const RUSH_SKIP_CHECKS_PARAMETER = '--rush-skip-checks';
function _reportErrorAndSetExitCode(error, terminal, debugEnabled) {
    var _a;
    if (!(error instanceof AlreadyReportedError)) {
        const prefix = 'ERROR: ';
        terminal.writeErrorLine('\n' + PrintUtilities.wrapWords(prefix + error.message));
    }
    if (debugEnabled) {
        // If catchSyncErrors() called this, then show a call stack similar to what Node.js
        // would show for an uncaught error
        terminal.writeErrorLine('\n' + error.stack);
    }
    process.exit((_a = process.exitCode) !== null && _a !== void 0 ? _a : 1);
}
export class RushPnpmCommandLineParser {
    constructor(options, terminal, debugEnabled) {
        this._debugEnabled = debugEnabled;
        this._terminal = terminal;
        // Are we in a Rush repo?
        const rushJsonFilePath = RushConfiguration.tryFindRushJsonLocation({
            // showVerbose is false because the logging message may break JSON output
            showVerbose: false
        });
        initializeDotEnv(terminal, rushJsonFilePath);
        const rushConfiguration = rushJsonFilePath
            ? RushConfiguration.loadFromConfigurationFile(rushJsonFilePath)
            : undefined;
        NodeJsCompatibility.warnAboutCompatibilityIssues({
            isRushLib: true,
            alreadyReportedNodeTooNewError: !!options.alreadyReportedNodeTooNewError,
            rushConfiguration
        });
        if (!rushConfiguration) {
            throw new Error('The "rush-pnpm" command must be executed in a folder that is under a Rush workspace folder');
        }
        this._rushConfiguration = rushConfiguration;
        if (rushConfiguration.packageManager !== 'pnpm') {
            throw new Error(`The "rush-pnpm" command requires your ${RushConstants.rushJsonFilename} to be configured to use the PNPM package manager`);
        }
        if (!rushConfiguration.pnpmOptions.useWorkspaces) {
            const pnpmConfigFilename = rushConfiguration.pnpmOptions.jsonFilename || RushConstants.rushJsonFilename;
            throw new Error(`The "rush-pnpm" command requires the "useWorkspaces" setting to be enabled in ${pnpmConfigFilename}`);
        }
        let pnpmArgs = [];
        let subspaceName = 'default';
        if (process.argv.indexOf('--subspace') >= 0) {
            if (process.argv[2] !== '--subspace') {
                throw new Error('If you want to specify a subspace, you should place "--subspace <subspace_name>" immediately after the "rush-pnpm" command');
            }
            subspaceName = process.argv[3];
            // 0 = node.exe
            // 1 = rush-pnpm
            // 2 = --subspace
            // 3 = <subspace_name>
            pnpmArgs = process.argv.slice(4);
        }
        else {
            // 0 = node.exe
            // 1 = rush-pnpm
            pnpmArgs = process.argv.slice(2);
        }
        this._pnpmArgs = pnpmArgs;
        const subspace = rushConfiguration.getSubspace(subspaceName);
        this._subspace = subspace;
        const workspaceFolder = subspace.getSubspaceTempFolderPath();
        const workspaceFilePath = path.join(workspaceFolder, 'pnpm-workspace.yaml');
        if (!FileSystem.exists(workspaceFilePath)) {
            this._terminal.writeErrorLine('Error: The PNPM workspace file has not been generated:');
            this._terminal.writeErrorLine(`  ${workspaceFilePath}\n`);
            this._terminal.writeLine(Colorize.cyan(`Do you need to run "rush install" or "rush update"?`));
            throw new AlreadyReportedError();
        }
        if (!FileSystem.exists(rushConfiguration.packageManagerToolFilename)) {
            this._terminal.writeErrorLine('Error: The PNPM local binary has not been installed yet.');
            this._terminal.writeLine('\n' + Colorize.cyan(`Do you need to run "rush install" or "rush update"?`));
            throw new AlreadyReportedError();
        }
    }
    static async initializeAsync(options) {
        var _a;
        const debugEnabled = process.argv.indexOf('--debug') >= 0;
        const verboseEnabled = process.argv.indexOf('--verbose') >= 0;
        const localTerminalProvider = (_a = options.terminalProvider) !== null && _a !== void 0 ? _a : new ConsoleTerminalProvider({
            debugEnabled,
            verboseEnabled
        });
        const terminal = new Terminal(localTerminalProvider);
        try {
            const rushPnpmCommandLineParser = new RushPnpmCommandLineParser(options, terminal, debugEnabled);
            await rushPnpmCommandLineParser._validatePnpmUsageAsync(rushPnpmCommandLineParser._pnpmArgs);
            return rushPnpmCommandLineParser;
        }
        catch (error) {
            _reportErrorAndSetExitCode(error, terminal, debugEnabled);
        }
    }
    async executeAsync() {
        // Node.js can sometimes accidentally terminate with a zero exit code  (e.g. for an uncaught
        // promise exception), so we start with the assumption that the exit code is 1
        // and set it to 0 only on success.
        process.exitCode = 1;
        await this._executeAsync();
        if (process.exitCode === 0) {
            await this._postExecuteAsync();
        }
    }
    async _validatePnpmUsageAsync(pnpmArgs) {
        if (pnpmArgs[0] === RUSH_SKIP_CHECKS_PARAMETER) {
            pnpmArgs.shift();
            // Ignore other checks
            return;
        }
        if (pnpmArgs.length === 0) {
            return;
        }
        const firstArg = pnpmArgs[0];
        // Detect common safe invocations
        if (pnpmArgs.includes('-h') || pnpmArgs.includes('--help') || pnpmArgs.includes('-?')) {
            return;
        }
        if (pnpmArgs.length === 1) {
            if (firstArg === '-v' || firstArg === '--version') {
                return;
            }
        }
        const BYPASS_NOTICE = `To bypass this check, add "${RUSH_SKIP_CHECKS_PARAMETER}" as the very first command line option.`;
        if (!/^[a-z]+([a-z0-9\-])*$/.test(firstArg)) {
            // We can't parse this CLI syntax
            this._terminal.writeErrorLine(`Warning: The "rush-pnpm" wrapper expects a command verb before "${firstArg}"\n`);
            this._terminal.writeLine(Colorize.cyan(BYPASS_NOTICE));
            throw new AlreadyReportedError();
        }
        else {
            const commandName = firstArg;
            // Also accept SKIP_RUSH_CHECKS_PARAMETER immediately after the command verb
            if (pnpmArgs[1] === RUSH_SKIP_CHECKS_PARAMETER) {
                pnpmArgs.splice(1, 1);
                return;
            }
            if (pnpmArgs.indexOf(RUSH_SKIP_CHECKS_PARAMETER) >= 0) {
                // We do not attempt to parse PNPM's complete CLI syntax, so we cannot be sure how to interpret
                // strings that appear outside of the specific patterns that this parser recognizes
                this._terminal.writeErrorLine(PrintUtilities.wrapWords(`Error: The "${RUSH_SKIP_CHECKS_PARAMETER}" option must be the first parameter for the "rush-pnpm" command.`));
                throw new AlreadyReportedError();
            }
            this._commandName = commandName;
            // Warn about commands known not to work
            /* eslint-disable no-fallthrough */
            switch (commandName) {
                // Blocked
                case 'import': {
                    this._terminal.writeErrorLine(PrintUtilities.wrapWords(`Error: The "pnpm ${commandName}" command is known to be incompatible with Rush's environment.`) + '\n');
                    this._terminal.writeLine(Colorize.cyan(BYPASS_NOTICE));
                    throw new AlreadyReportedError();
                }
                // Show warning for install commands
                case 'add':
                case 'install':
                /* synonym */
                case 'i':
                case 'install-test':
                /* synonym */
                case 'it': {
                    this._terminal.writeErrorLine(PrintUtilities.wrapWords(`Error: The "pnpm ${commandName}" command is incompatible with Rush's environment.` +
                        ` Use the "rush install" or "rush update" commands instead.`) + '\n');
                    this._terminal.writeLine(Colorize.cyan(BYPASS_NOTICE));
                    throw new AlreadyReportedError();
                }
                // Show warning
                case 'link':
                /* synonym */
                case 'ln':
                case 'remove':
                /* synonym */
                case 'rm':
                case 'unlink':
                case 'update':
                /* synonym */
                case 'up': {
                    this._terminal.writeWarningLine(PrintUtilities.wrapWords(`Warning: The "pnpm ${commandName}" command makes changes that may invalidate Rush's workspace state.`) + '\n');
                    this._terminal.writeWarningLine(`==> Consider running "rush install" or "rush update" afterwards.\n`);
                    break;
                }
                // Know safe after validation
                case 'patch': {
                    const semver = await import('semver');
                    /**
                     * If you were to accidentally attempt to use rush-pnpm patch with a pnpmVersion < 7.4.0, pnpm patch may fallback to the system patch command.
                     * For instance, /usr/bin/patch which may just hangs forever
                     * So, erroring out the command if the pnpm version is < 7.4.0
                     */
                    if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '7.4.0')) {
                        this._terminal.writeErrorLine(PrintUtilities.wrapWords(`Error: The "pnpm patch" command is added after pnpm@7.4.0.` +
                            ` Please update "pnpmVersion" >= 7.4.0 in ${RushConstants.rushJsonFilename} file and run "rush update" to use this command.`) + '\n');
                        throw new AlreadyReportedError();
                    }
                    break;
                }
                case 'patch-commit': {
                    const pnpmOptionsJsonFilename = path.join(this._rushConfiguration.commonRushConfigFolder, RushConstants.pnpmConfigFilename);
                    if (this._rushConfiguration.rushConfigurationJson.pnpmOptions) {
                        this._terminal.writeErrorLine(PrintUtilities.wrapWords(`Error: The "pnpm patch-commit" command is incompatible with specifying "pnpmOptions" in ${RushConstants.rushJsonFilename} file.` +
                            ` Please move the content of "pnpmOptions" in ${RushConstants.rushJsonFilename} file to ${pnpmOptionsJsonFilename}`) + '\n');
                        throw new AlreadyReportedError();
                    }
                    break;
                }
                case 'patch-remove': {
                    const semver = await import('semver');
                    /**
                     * The "patch-remove" command was introduced in pnpm version 8.5.0
                     */
                    if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '8.5.0')) {
                        this._terminal.writeErrorLine(PrintUtilities.wrapWords(`Error: The "pnpm patch-remove" command is added after pnpm@8.5.0.` +
                            ` Please update "pnpmVersion" >= 8.5.0 in ${RushConstants.rushJsonFilename} file and run "rush update" to use this command.`) + '\n');
                        throw new AlreadyReportedError();
                    }
                    break;
                }
                case 'approve-builds': {
                    const semver = await import('semver');
                    /**
                     * The "approve-builds" command was introduced in pnpm version 10.1.0
                     * to approve packages for running build scripts when onlyBuiltDependencies is used
                     */
                    if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '10.1.0')) {
                        this._terminal.writeErrorLine(PrintUtilities.wrapWords(`Error: The "pnpm approve-builds" command is added after pnpm@10.1.0.` +
                            ` Please update "pnpmVersion" >= 10.1.0 in ${RushConstants.rushJsonFilename} file and run "rush update" to use this command.`) + '\n');
                        throw new AlreadyReportedError();
                    }
                    const pnpmOptionsJsonFilename = path.join(this._rushConfiguration.commonRushConfigFolder, RushConstants.pnpmConfigFilename);
                    if (this._rushConfiguration.rushConfigurationJson.pnpmOptions) {
                        this._terminal.writeErrorLine(PrintUtilities.wrapWords(`Error: The "pnpm approve-builds" command is incompatible with specifying "pnpmOptions" in ${RushConstants.rushJsonFilename} file.` +
                            ` Please move the content of "pnpmOptions" in ${RushConstants.rushJsonFilename} file to ${pnpmOptionsJsonFilename}`) + '\n');
                        throw new AlreadyReportedError();
                    }
                    break;
                }
                // Known safe
                case 'audit':
                case 'exec':
                case 'list':
                /* synonym */
                case 'ls':
                case 'outdated':
                case 'pack':
                case 'prune':
                case 'publish':
                case 'rebuild':
                /* synonym */
                case 'rb':
                case 'root':
                case 'run':
                case 'start':
                case 'store':
                case 'test':
                /* synonym */
                case 't':
                case 'why': {
                    break;
                }
                // Unknown
                default: {
                    this._terminal.writeErrorLine(PrintUtilities.wrapWords(`Error: The "pnpm ${commandName}" command has not been tested with Rush's environment. It may be incompatible.`) + '\n');
                    this._terminal.writeLine(Colorize.cyan(BYPASS_NOTICE));
                }
            }
            /* eslint-enable no-fallthrough */
        }
    }
    async _executeAsync() {
        const rushConfiguration = this._rushConfiguration;
        const workspaceFolder = this._subspace.getSubspaceTempFolderPath();
        const pnpmEnvironmentMap = new EnvironmentMap(process.env);
        pnpmEnvironmentMap.set('NPM_CONFIG_WORKSPACE_DIR', workspaceFolder);
        if (rushConfiguration.pnpmOptions.pnpmStorePath) {
            pnpmEnvironmentMap.set('NPM_CONFIG_STORE_DIR', rushConfiguration.pnpmOptions.pnpmStorePath);
            pnpmEnvironmentMap.set('NPM_CONFIG_CACHE_DIR', rushConfiguration.pnpmOptions.pnpmStorePath);
            pnpmEnvironmentMap.set('NPM_CONFIG_STATE_DIR', rushConfiguration.pnpmOptions.pnpmStorePath);
        }
        if (rushConfiguration.pnpmOptions.environmentVariables) {
            for (const [envKey, { value: envValue, override }] of Object.entries(rushConfiguration.pnpmOptions.environmentVariables)) {
                if (override) {
                    pnpmEnvironmentMap.set(envKey, envValue);
                }
                else {
                    if (undefined === pnpmEnvironmentMap.get(envKey)) {
                        pnpmEnvironmentMap.set(envKey, envValue);
                    }
                }
            }
        }
        let onStdoutStreamChunk;
        switch (this._commandName) {
            case 'patch': {
                // Replace `pnpm patch-commit` with `rush-pnpm patch-commit` when running
                // `pnpm patch` to avoid the `pnpm patch` command being suggested in the output
                onStdoutStreamChunk = (stdoutChunk) => {
                    return stdoutChunk.replace(/pnpm patch-commit/g, `rush-pnpm --subspace ${this._subspace.subspaceName} patch-commit`);
                };
                break;
            }
        }
        try {
            const { exitCode } = await Utilities.executeCommandAsync({
                command: rushConfiguration.packageManagerToolFilename,
                args: this._pnpmArgs,
                workingDirectory: process.cwd(),
                environment: pnpmEnvironmentMap.toObject(),
                keepEnvironment: true,
                onStdoutStreamChunk,
                captureExitCodeAndSignal: true
            });
            if (typeof exitCode === 'number') {
                process.exitCode = exitCode;
            }
            else {
                // If the exit code is not a number, the process was terminated by a signal
                process.exitCode = 1;
            }
        }
        catch (e) {
            this._terminal.writeDebugLine(`Error: ${e}`);
        }
    }
    async _postExecuteAsync() {
        var _a, _b;
        const commandName = this._commandName;
        if (!commandName) {
            return;
        }
        const subspaceTempFolder = this._subspace.getSubspaceTempFolderPath();
        switch (commandName) {
            case 'patch-remove':
            case 'patch-commit': {
                // why need to throw error when pnpm-config.json not exists?
                // 1. pnpm-config.json is required for `rush-pnpm patch-commit`. Rush writes the patched dependency to the pnpm-config.json when finishes.
                // 2. we can not fallback to use Monorepo config folder (common/config/rush) due to that this command is intended to apply to input subspace only.
                //    It will produce unexpected behavior if we use the fallback.
                if (this._subspace.getPnpmOptions() === undefined) {
                    const subspaceConfigFolder = this._subspace.getSubspaceConfigFolderPath();
                    this._terminal.writeErrorLine(`The "rush-pnpm patch-commit" command cannot proceed without a pnpm-config.json file.` +
                        `  Create one in this folder: ${subspaceConfigFolder}`);
                    break;
                }
                // Example: "C:\MyRepo\common\temp\package.json"
                const commonPackageJsonFilename = `${subspaceTempFolder}/${FileConstants.PackageJson}`;
                const commonPackageJson = JsonFile.load(commonPackageJsonFilename);
                const newGlobalPatchedDependencies = (_a = commonPackageJson === null || commonPackageJson === void 0 ? void 0 : commonPackageJson.pnpm) === null || _a === void 0 ? void 0 : _a.patchedDependencies;
                const pnpmOptions = this._subspace.getPnpmOptions();
                const currentGlobalPatchedDependencies = pnpmOptions === null || pnpmOptions === void 0 ? void 0 : pnpmOptions.globalPatchedDependencies;
                if (!Objects.areDeepEqual(currentGlobalPatchedDependencies, newGlobalPatchedDependencies)) {
                    const commonTempPnpmPatchesFolder = `${subspaceTempFolder}/${RushConstants.pnpmPatchesFolderName}`;
                    const rushPnpmPatchesFolder = this._subspace.getSubspacePnpmPatchesFolderPath();
                    // Copy (or delete) common\temp\subspace\patches\ --> common\config\pnpm-patches\ OR common\config\rush\pnpm-patches\
                    if (FileSystem.exists(commonTempPnpmPatchesFolder)) {
                        FileSystem.ensureEmptyFolder(rushPnpmPatchesFolder);
                        // eslint-disable-next-line no-console
                        console.log(`Copying ${commonTempPnpmPatchesFolder}`);
                        // eslint-disable-next-line no-console
                        console.log(`  --> ${rushPnpmPatchesFolder}`);
                        FileSystem.copyFiles({
                            sourcePath: commonTempPnpmPatchesFolder,
                            destinationPath: rushPnpmPatchesFolder
                        });
                    }
                    else {
                        if (FileSystem.exists(rushPnpmPatchesFolder)) {
                            // eslint-disable-next-line no-console
                            console.log(`Deleting ${rushPnpmPatchesFolder}`);
                            FileSystem.deleteFolder(rushPnpmPatchesFolder);
                        }
                    }
                    // Update patchedDependencies to pnpm configuration file
                    pnpmOptions === null || pnpmOptions === void 0 ? void 0 : pnpmOptions.updateGlobalPatchedDependencies(newGlobalPatchedDependencies);
                    // Rerun installation to update
                    await this._doRushUpdateAsync();
                    this._terminal.writeWarningLine(`Rush refreshed the ${RushConstants.pnpmConfigFilename}, shrinkwrap file and patch files under the ` +
                        `"${commonTempPnpmPatchesFolder}" folder.\n` +
                        '  Please commit this change to Git.');
                }
                break;
            }
            case 'approve-builds': {
                if (this._subspace.getPnpmOptions() === undefined) {
                    const subspaceConfigFolder = this._subspace.getSubspaceConfigFolderPath();
                    this._terminal.writeErrorLine(`The "rush-pnpm approve-builds" command cannot proceed without a pnpm-config.json file.` +
                        `  Create one in this folder: ${subspaceConfigFolder}`);
                    break;
                }
                // Example: "C:\MyRepo\common\temp\package.json"
                const commonPackageJsonFilename = `${subspaceTempFolder}/${FileConstants.PackageJson}`;
                const commonPackageJson = await JsonFile.loadAsync(commonPackageJsonFilename);
                const newGlobalOnlyBuiltDependencies = (_b = commonPackageJson === null || commonPackageJson === void 0 ? void 0 : commonPackageJson.pnpm) === null || _b === void 0 ? void 0 : _b.onlyBuiltDependencies;
                const pnpmOptions = this._subspace.getPnpmOptions();
                const currentGlobalOnlyBuiltDependencies = pnpmOptions === null || pnpmOptions === void 0 ? void 0 : pnpmOptions.globalOnlyBuiltDependencies;
                if (!Objects.areDeepEqual(currentGlobalOnlyBuiltDependencies, newGlobalOnlyBuiltDependencies)) {
                    // Update onlyBuiltDependencies to pnpm configuration file
                    pnpmOptions === null || pnpmOptions === void 0 ? void 0 : pnpmOptions.updateGlobalOnlyBuiltDependencies(newGlobalOnlyBuiltDependencies);
                    // Rerun installation to update
                    await this._doRushUpdateAsync();
                    this._terminal.writeWarningLine(`Rush refreshed the ${RushConstants.pnpmConfigFilename} and shrinkwrap file.\n` +
                        '  Please commit this change to Git.');
                }
                break;
            }
        }
    }
    async _doRushUpdateAsync() {
        this._terminal.writeLine();
        this._terminal.writeLine(Colorize.green('Running "rush update"'));
        this._terminal.writeLine();
        const rushGlobalFolder = new RushGlobalFolder();
        const purgeManager = new PurgeManager(this._rushConfiguration, rushGlobalFolder);
        const installManagerOptions = {
            debug: this._debugEnabled,
            allowShrinkwrapUpdates: true,
            bypassPolicy: false,
            noLink: false,
            fullUpgrade: false,
            recheckShrinkwrap: true,
            networkConcurrency: undefined,
            offline: false,
            collectLogFile: false,
            variant: process.env[EnvironmentVariableNames.RUSH_VARIANT], // For `rush-pnpm`, only use the env var
            maxInstallAttempts: RushConstants.defaultMaxInstallAttempts,
            pnpmFilterArgumentValues: [],
            selectedProjects: new Set(this._rushConfiguration.projects),
            checkOnly: false,
            subspace: this._subspace,
            terminal: this._terminal
        };
        const installManagerFactoryModule = await import(
        /* webpackChunkName: 'InstallManagerFactory' */
        '../logic/InstallManagerFactory');
        const installManager = await installManagerFactoryModule.InstallManagerFactory.getInstallManagerAsync(this._rushConfiguration, rushGlobalFolder, purgeManager, installManagerOptions);
        try {
            await installManager.doInstallAsync();
        }
        finally {
            await purgeManager.startDeleteAllAsync();
        }
    }
}
//# sourceMappingURL=RushPnpmCommandLineParser.js.map