// 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 { CommandLineParser, CommandLineHelper } from '@rushstack/ts-command-line';
import { InternalError, AlreadyReportedError, Text } from '@rushstack/node-core-library';
import { ConsoleTerminalProvider, Terminal, PrintUtilities, Colorize } from '@rushstack/terminal';
import { RushConfiguration } from '../api/RushConfiguration';
import { RushConstants } from '../logic/RushConstants';
import { CommandLineConfiguration } from '../api/CommandLineConfiguration';
import { AddAction } from './actions/AddAction';
import { AlertAction } from './actions/AlertAction';
import { BridgePackageAction } from './actions/BridgePackageAction';
import { ChangeAction } from './actions/ChangeAction';
import { CheckAction } from './actions/CheckAction';
import { DeployAction } from './actions/DeployAction';
import { InitAction } from './actions/InitAction';
import { InitAutoinstallerAction } from './actions/InitAutoinstallerAction';
import { InitDeployAction } from './actions/InitDeployAction';
import { InstallAction } from './actions/InstallAction';
import { InstallAutoinstallerAction } from './actions/InstallAutoinstallerAction';
import { LinkAction } from './actions/LinkAction';
import { LinkPackageAction } from './actions/LinkPackageAction';
import { ListAction } from './actions/ListAction';
import { PublishAction } from './actions/PublishAction';
import { PurgeAction } from './actions/PurgeAction';
import { RemoveAction } from './actions/RemoveAction';
import { ScanAction } from './actions/ScanAction';
import { UnlinkAction } from './actions/UnlinkAction';
import { UpdateAction } from './actions/UpdateAction';
import { UpdateAutoinstallerAction } from './actions/UpdateAutoinstallerAction';
import { UpdateCloudCredentialsAction } from './actions/UpdateCloudCredentialsAction';
import { UpgradeInteractiveAction } from './actions/UpgradeInteractiveAction';
import { VersionAction } from './actions/VersionAction';
import { GlobalScriptAction } from './scriptActions/GlobalScriptAction';
import { PhasedScriptAction } from './scriptActions/PhasedScriptAction';
import { Telemetry } from '../logic/Telemetry';
import { RushGlobalFolder } from '../api/RushGlobalFolder';
import { NodeJsCompatibility } from '../logic/NodeJsCompatibility';
import { SetupAction } from './actions/SetupAction';
import { PluginManager } from '../pluginFramework/PluginManager';
import { RushSession } from '../pluginFramework/RushSession';
import { InitSubspaceAction } from './actions/InitSubspaceAction';
import { RushAlerts } from '../utilities/RushAlerts';
import { initializeDotEnv } from '../logic/dotenv';
import { measureAsyncFn } from '../utilities/performance';
export class RushCommandLineParser extends CommandLineParser {
    /**
     * The current working directory that was used to find the Rush configuration.
     */
    get cwd() {
        return this._rushOptions.cwd;
    }
    constructor(options) {
        super({
            toolFilename: 'rush',
            toolDescription: 'Rush makes life easier for JavaScript developers who develop, build, and publish' +
                ' many packages from a central Git repo.  It is designed to handle very large repositories' +
                ' supporting many projects and people.  Rush provides policies, protections, and customizations' +
                ' that help coordinate teams and safely onboard new contributors.  Rush also generates change logs' +
                ' and automates package publishing.  It can manage decoupled subsets of projects with different' +
                ' release and versioning strategies.  A full API is included to facilitate integration with other' +
                ' automation tools.  If you are looking for a proven turnkey solution for monorepo management,' +
                ' Rush is for you.',
            enableTabCompletionAction: true
        });
        this._restrictConsoleOutput = RushCommandLineParser.shouldRestrictConsoleOutput();
        this._debugParameter = this.defineFlagParameter({
            parameterLongName: '--debug',
            parameterShortName: '-d',
            description: 'Show the full call stack if an error occurs while executing the tool'
        });
        this._quietParameter = this.defineFlagParameter({
            parameterLongName: '--quiet',
            parameterShortName: '-q',
            description: 'Hide rush startup information'
        });
        const terminalProvider = new ConsoleTerminalProvider();
        this._terminalProvider = terminalProvider;
        const terminal = new Terminal(this._terminalProvider);
        this._terminal = terminal;
        this._rushOptions = this._normalizeOptions(options || {});
        const { cwd, alreadyReportedNodeTooNewError, builtInPluginConfigurations } = this._rushOptions;
        let rushJsonFilePath;
        try {
            rushJsonFilePath = RushConfiguration.tryFindRushJsonLocation({
                startingFolder: cwd,
                showVerbose: !this._restrictConsoleOutput
            });
            initializeDotEnv(terminal, rushJsonFilePath);
            if (rushJsonFilePath) {
                this.rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFilePath);
            }
        }
        catch (error) {
            this._reportErrorAndSetExitCode(error);
        }
        NodeJsCompatibility.warnAboutCompatibilityIssues({
            isRushLib: true,
            alreadyReportedNodeTooNewError,
            rushConfiguration: this.rushConfiguration
        });
        this.rushGlobalFolder = new RushGlobalFolder();
        this.rushSession = new RushSession({
            getIsDebugMode: () => this.isDebug,
            terminalProvider
        });
        this.pluginManager = new PluginManager({
            rushSession: this.rushSession,
            rushConfiguration: this.rushConfiguration,
            terminal,
            builtInPluginConfigurations,
            restrictConsoleOutput: this._restrictConsoleOutput,
            rushGlobalFolder: this.rushGlobalFolder
        });
        const pluginCommandLineConfigurations = this.pluginManager.tryGetCustomCommandLineConfigurationInfos();
        const hasBuildCommandInPlugin = pluginCommandLineConfigurations.some((x) => x.commandLineConfiguration.commands.has(RushConstants.buildCommandName));
        // If the plugin has a build command, we don't need to autocreate the default build command.
        this._autocreateBuildCommand = !hasBuildCommandInPlugin;
        this._populateActions();
        for (const { commandLineConfiguration, pluginLoader } of pluginCommandLineConfigurations) {
            try {
                this._addCommandLineConfigActions(commandLineConfiguration);
            }
            catch (e) {
                this._reportErrorAndSetExitCode(new Error(`Error from plugin ${pluginLoader.pluginName} by ${pluginLoader.packageName}: ${e.toString()}`));
            }
        }
    }
    get isDebug() {
        return this._debugParameter.value;
    }
    get isQuiet() {
        return this._quietParameter.value;
    }
    get terminal() {
        return this._terminal;
    }
    /**
     * Utility to determine if the app should restrict writing to the console.
     */
    static shouldRestrictConsoleOutput() {
        if (CommandLineHelper.isTabCompletionActionRequest(process.argv)) {
            return true;
        }
        for (let i = 2; i < process.argv.length; i++) {
            const arg = process.argv[i];
            if (arg === '-q' || arg === '--quiet' || arg === '--json') {
                return true;
            }
        }
        return false;
    }
    flushTelemetry() {
        var _a;
        (_a = this.telemetry) === null || _a === void 0 ? void 0 : _a.flush();
    }
    async executeAsync(args) {
        // debugParameter will be correctly parsed during super.executeAsync(), so manually parse here.
        this._terminalProvider.verboseEnabled = this._terminalProvider.debugEnabled =
            process.argv.indexOf('--debug') >= 0;
        await measureAsyncFn('rush:initializeUnassociatedPlugins', () => this.pluginManager.tryInitializeUnassociatedPluginsAsync());
        return await super.executeAsync(args);
    }
    async onExecuteAsync() {
        var _a;
        // Defensively set the exit code to 1 so if Rush crashes for whatever reason, we'll have a nonzero exit code.
        // For example, Node.js currently has the inexcusable design of terminating with zero exit code when
        // there is an uncaught promise exception.  This will supposedly be fixed in Node.js 9.
        // Ideally we should do this for all the Rush actions, but "rush build" is the most critical one
        // -- if it falsely appears to succeed, we could merge bad PRs, publish empty packages, etc.
        process.exitCode = 1;
        if (this._debugParameter.value) {
            InternalError.breakInDebugger = true;
        }
        try {
            await this._wrapOnExecuteAsync();
            // TODO: rushConfiguration is typed as "!: RushConfiguration" here, but can sometimes be undefined
            if (this.rushConfiguration) {
                try {
                    const { configuration: experiments } = this.rushConfiguration.experimentsConfiguration;
                    if (experiments.rushAlerts) {
                        // TODO: Fix this
                        // eslint-disable-next-line @typescript-eslint/no-explicit-any
                        const actionName = this
                            ._getArgumentParser()
                            .parseArgs(process.argv.slice(2)).action;
                        // only display alerts when certain specific actions are triggered
                        if (RushAlerts.alertTriggerActions.includes(actionName)) {
                            this._terminal.writeDebugLine('Checking Rush alerts...');
                            const rushAlerts = await RushAlerts.loadFromConfigurationAsync(this.rushConfiguration, this._terminal);
                            // Print out alerts if have after each successful command actions
                            await rushAlerts.printAlertsAsync();
                        }
                    }
                }
                catch (error) {
                    if (error instanceof AlreadyReportedError) {
                        throw error;
                    }
                    // Generally the RushAlerts implementation should handle its own error reporting; if not,
                    // clarify the source, since the Rush Alerts behavior is nondeterministic and may not repro easily:
                    this._terminal.writeErrorLine(`\nAn unexpected error was encountered by the Rush alerts feature:`);
                    this._terminal.writeErrorLine(error.message);
                    throw new AlreadyReportedError();
                }
            }
            // If we make it here, everything went fine, so reset the exit code back to 0
            process.exitCode = 0;
        }
        catch (error) {
            this._reportErrorAndSetExitCode(error);
        }
        // This only gets hit if the wrapped execution completes successfully
        await ((_a = this.telemetry) === null || _a === void 0 ? void 0 : _a.ensureFlushedAsync());
    }
    _normalizeOptions(options) {
        return {
            cwd: options.cwd || process.cwd(),
            alreadyReportedNodeTooNewError: options.alreadyReportedNodeTooNewError || false,
            builtInPluginConfigurations: options.builtInPluginConfigurations || []
        };
    }
    async _wrapOnExecuteAsync() {
        if (this.rushConfiguration) {
            this.telemetry = new Telemetry(this.rushConfiguration, this.rushSession);
        }
        try {
            await measureAsyncFn('rush:commandLineParser:onExecuteAsync', () => super.onExecuteAsync());
        }
        finally {
            if (this.telemetry) {
                this.flushTelemetry();
            }
        }
    }
    _populateActions() {
        try {
            // Alphabetical order
            this.addAction(new AddAction(this));
            this.addAction(new ChangeAction(this));
            this.addAction(new CheckAction(this));
            this.addAction(new DeployAction(this));
            this.addAction(new InitAction(this));
            this.addAction(new InitAutoinstallerAction(this));
            this.addAction(new InitDeployAction(this));
            this.addAction(new InitSubspaceAction(this));
            this.addAction(new InstallAction(this));
            this.addAction(new LinkAction(this));
            this.addAction(new ListAction(this));
            this.addAction(new PublishAction(this));
            this.addAction(new PurgeAction(this));
            this.addAction(new RemoveAction(this));
            this.addAction(new ScanAction(this));
            this.addAction(new SetupAction(this));
            this.addAction(new UnlinkAction(this));
            this.addAction(new UpdateAction(this));
            this.addAction(new InstallAutoinstallerAction(this));
            this.addAction(new UpdateAutoinstallerAction(this));
            this.addAction(new UpdateCloudCredentialsAction(this));
            this.addAction(new UpgradeInteractiveAction(this));
            this.addAction(new VersionAction(this));
            this.addAction(new AlertAction(this));
            this.addAction(new BridgePackageAction(this));
            this.addAction(new LinkPackageAction(this));
            this._populateScriptActions();
        }
        catch (error) {
            this._reportErrorAndSetExitCode(error);
        }
    }
    _populateScriptActions() {
        // If there is not a rush.json file, we still want "build" and "rebuild" to appear in the
        // command-line help
        let commandLineConfigFilePath;
        if (this.rushConfiguration) {
            commandLineConfigFilePath = path.join(this.rushConfiguration.commonRushConfigFolder, RushConstants.commandLineFilename);
        }
        // If a build action is already added by a plugin, we don't want to add a default "build" script
        const doNotIncludeDefaultBuildCommands = !this._autocreateBuildCommand;
        const commandLineConfiguration = CommandLineConfiguration.loadFromFileOrDefault(commandLineConfigFilePath, doNotIncludeDefaultBuildCommands);
        this._addCommandLineConfigActions(commandLineConfiguration);
    }
    _addCommandLineConfigActions(commandLineConfiguration) {
        // Register each custom command
        for (const command of commandLineConfiguration.commands.values()) {
            this._addCommandLineConfigAction(commandLineConfiguration, command);
        }
    }
    _addCommandLineConfigAction(commandLineConfiguration, command) {
        if (this.tryGetAction(command.name)) {
            throw new Error(`${RushConstants.commandLineFilename} defines a command "${command.name}"` +
                ` using a name that already exists`);
        }
        switch (command.commandKind) {
            case RushConstants.globalCommandKind: {
                this._addGlobalScriptAction(commandLineConfiguration, command);
                break;
            }
            case RushConstants.phasedCommandKind: {
                this._addPhasedCommandLineConfigAction(commandLineConfiguration, command);
                break;
            }
            default:
                throw new Error(`${RushConstants.commandLineFilename} defines a command "${command.name}"` +
                    ` using an unsupported command kind "${command.commandKind}"`);
        }
    }
    _getSharedCommandActionOptions(commandLineConfiguration, command) {
        return {
            actionName: command.name,
            summary: command.summary,
            documentation: command.description || command.summary,
            safeForSimultaneousRushProcesses: command.safeForSimultaneousRushProcesses,
            command,
            parser: this,
            commandLineConfiguration: commandLineConfiguration
        };
    }
    _addGlobalScriptAction(commandLineConfiguration, command) {
        if (command.name === RushConstants.buildCommandName ||
            command.name === RushConstants.rebuildCommandName) {
            throw new Error(`${RushConstants.commandLineFilename} defines a command "${command.name}" using ` +
                `the command kind "${RushConstants.globalCommandKind}". This command can only be designated as a command ` +
                `kind "${RushConstants.bulkCommandKind}" or "${RushConstants.phasedCommandKind}".`);
        }
        const sharedCommandOptions = this._getSharedCommandActionOptions(commandLineConfiguration, command);
        this.addAction(new GlobalScriptAction({
            ...sharedCommandOptions,
            shellCommand: command.shellCommand,
            autoinstallerName: command.autoinstallerName
        }));
    }
    _addPhasedCommandLineConfigAction(commandLineConfiguration, command) {
        var _a, _b;
        const baseCommandOptions = this._getSharedCommandActionOptions(commandLineConfiguration, command);
        this.addAction(new PhasedScriptAction({
            ...baseCommandOptions,
            enableParallelism: command.enableParallelism,
            incremental: command.incremental || false,
            disableBuildCache: command.disableBuildCache || false,
            // The Async.forEachAsync() API defaults allowOversubscription=false, whereas Rush historically
            // defaults allowOversubscription=true to favor faster builds rather than strictly staying below
            // the CPU limit.
            allowOversubscription: (_a = command.allowOversubscription) !== null && _a !== void 0 ? _a : true,
            initialPhases: command.phases,
            originalPhases: command.originalPhases,
            watchPhases: command.watchPhases,
            watchDebounceMs: (_b = command.watchDebounceMs) !== null && _b !== void 0 ? _b : RushConstants.defaultWatchDebounceMs,
            phases: commandLineConfiguration.phases,
            alwaysWatch: command.alwaysWatch,
            alwaysInstall: command.alwaysInstall
        }));
    }
    _reportErrorAndSetExitCode(error) {
        if (!(error instanceof AlreadyReportedError)) {
            const prefix = 'ERROR: ';
            // The colors package will eat multi-newlines, which could break formatting
            // in user-specified messages and instructions, so we prefer to color each
            // line individually.
            const message = Text.splitByNewLines(PrintUtilities.wrapWords(prefix + error.message))
                .map((line) => Colorize.red(line))
                .join('\n');
            // eslint-disable-next-line no-console
            console.error(`\n${message}`);
        }
        if (this._debugParameter.value) {
            // If catchSyncErrors() called this, then show a call stack similar to what Node.js
            // would show for an uncaught error
            // eslint-disable-next-line no-console
            console.error(`\n${error.stack}`);
        }
        this.flushTelemetry();
        const handleExit = () => {
            // Ideally we want to eliminate all calls to process.exit() from our code, and replace them
            // with normal control flow that properly cleans up its data structures.
            // For this particular call, we have a problem that the RushCommandLineParser constructor
            // performs nontrivial work that can throw an exception.  Either the Rush class would need
            // to handle reporting for those exceptions, or else _populateActions() should be moved
            // to a RushCommandLineParser lifecycle stage that can handle it.
            if (process.exitCode !== undefined) {
                process.exit(process.exitCode);
            }
            else {
                process.exit(1);
            }
        };
        if (this.telemetry && this.rushSession.hooks.flushTelemetry.isUsed()) {
            this.telemetry.ensureFlushedAsync().then(handleExit).catch(handleExit);
        }
        else {
            handleExit();
        }
    }
}
//# sourceMappingURL=RushCommandLineParser.js.map