"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.PhasedScriptAction = void 0;
const node_core_library_1 = require("@rushstack/node-core-library");
const terminal_1 = require("@rushstack/terminal");
const PhasedCommandHooks_1 = require("../../pluginFramework/PhasedCommandHooks");
const SetupChecks_1 = require("../../logic/SetupChecks");
const Stopwatch_1 = require("../../utilities/Stopwatch");
const BaseScriptAction_1 = require("./BaseScriptAction");
const OperationExecutionManager_1 = require("../../logic/operations/OperationExecutionManager");
const RushConstants_1 = require("../../logic/RushConstants");
const EnvironmentConfiguration_1 = require("../../api/EnvironmentConfiguration");
const BuildCacheConfiguration_1 = require("../../api/BuildCacheConfiguration");
const SelectionParameterSet_1 = require("../parsing/SelectionParameterSet");
const associateParametersByPhase_1 = require("../parsing/associateParametersByPhase");
const PhasedOperationPlugin_1 = require("../../logic/operations/PhasedOperationPlugin");
const ShellOperationRunnerPlugin_1 = require("../../logic/operations/ShellOperationRunnerPlugin");
const EventHooks_1 = require("../../api/EventHooks");
const ProjectChangeAnalyzer_1 = require("../../logic/ProjectChangeAnalyzer");
const OperationStatus_1 = require("../../logic/operations/OperationStatus");
const OperationResultSummarizerPlugin_1 = require("../../logic/operations/OperationResultSummarizerPlugin");
const ParseParallelism_1 = require("../parsing/ParseParallelism");
const CobuildConfiguration_1 = require("../../api/CobuildConfiguration");
const CacheableOperationPlugin_1 = require("../../logic/operations/CacheableOperationPlugin");
const RushProjectConfiguration_1 = require("../../api/RushProjectConfiguration");
const LegacySkipPlugin_1 = require("../../logic/operations/LegacySkipPlugin");
const ValidateOperationsPlugin_1 = require("../../logic/operations/ValidateOperationsPlugin");
const ShardedPhaseOperationPlugin_1 = require("../../logic/operations/ShardedPhaseOperationPlugin");
const FlagFile_1 = require("../../api/FlagFile");
const WeightedOperationPlugin_1 = require("../../logic/operations/WeightedOperationPlugin");
const Variants_1 = require("../../api/Variants");
const Selection_1 = require("../../logic/Selection");
const NodeDiagnosticDirPlugin_1 = require("../../logic/operations/NodeDiagnosticDirPlugin");
const IgnoredParametersPlugin_1 = require("../../logic/operations/IgnoredParametersPlugin");
const DebugHashesPlugin_1 = require("../../logic/operations/DebugHashesPlugin");
const performance_1 = require("../../utilities/performance");
const PERF_PREFIX = 'rush:phasedScriptAction';
/**
 * This class implements phased commands which are run individually for each project in the repo,
 * possibly in parallel, and which may define multiple phases.
 *
 * @remarks
 * Phased commands can be defined via common/config/command-line.json.  Rush's predefined "build"
 * and "rebuild" commands are also modeled as phased commands with a single phase that invokes the npm
 * "build" script for each project.
 */
class PhasedScriptAction extends BaseScriptAction_1.BaseScriptAction {
    constructor(options) {
        var _a;
        super(options);
        this._enableParallelism = options.enableParallelism;
        this._allowOversubscription = options.allowOversubscription;
        this._isIncrementalBuildAllowed = options.incremental;
        this._disableBuildCache = options.disableBuildCache;
        this._originalPhases = options.originalPhases;
        this._initialPhases = options.initialPhases;
        this._watchPhases = options.watchPhases;
        this._watchDebounceMs = (_a = options.watchDebounceMs) !== null && _a !== void 0 ? _a : RushConstants_1.RushConstants.defaultWatchDebounceMs;
        this._alwaysWatch = options.alwaysWatch;
        this._alwaysInstall = options.alwaysInstall;
        this._runsBeforeInstall = false;
        this._knownPhases = options.phases;
        this._changedProjectsOnly = false;
        this.sessionAbortController = new AbortController();
        this._executionAbortController = undefined;
        this.sessionAbortController.signal.addEventListener('abort', () => {
            var _a;
            (_a = this._executionAbortController) === null || _a === void 0 ? void 0 : _a.abort();
        }, { once: true });
        this.hooks = new PhasedCommandHooks_1.PhasedCommandHooks();
        this._terminal = new terminal_1.Terminal(this.rushSession.terminalProvider);
        this._parallelismParameter = this._enableParallelism
            ? this.defineStringParameter({
                parameterLongName: '--parallelism',
                parameterShortName: '-p',
                argumentName: 'COUNT',
                environmentVariable: EnvironmentConfiguration_1.EnvironmentVariableNames.RUSH_PARALLELISM,
                description: 'Specifies the maximum number of concurrent processes to launch during a build.' +
                    ' The COUNT should be a positive integer, a percentage value (eg. "50%") or the word "max"' +
                    ' to specify a count that is equal to the number of CPU cores. If this parameter is omitted,' +
                    ' then the default value depends on the operating system and number of CPU cores.'
            })
            : undefined;
        this._timelineParameter = this.defineFlagParameter({
            parameterLongName: '--timeline',
            description: 'After the build is complete, print additional statistics and CPU usage information,' +
                ' including an ASCII chart of the start and stop times for each operation.'
        });
        this._cobuildPlanParameter = this.defineFlagParameter({
            parameterLongName: '--log-cobuild-plan',
            description: '(EXPERIMENTAL) Before the build starts, log information about the cobuild state. This will include information about ' +
                'clusters and the projects that are part of each cluster.'
        });
        this._selectionParameters = new SelectionParameterSet_1.SelectionParameterSet(this.rushConfiguration, this, {
            gitOptions: {
                // Include lockfile processing since this expands the selection, and we need to select
                // at least the same projects selected with the same query to "rush build"
                includeExternalDependencies: true,
                // Enable filtering to reduce evaluation cost
                enableFiltering: true
            },
            includeSubspaceSelector: false,
            cwd: this.parser.cwd
        });
        this._verboseParameter = this.defineFlagParameter({
            parameterLongName: '--verbose',
            parameterShortName: '-v',
            description: 'Display the logs during the build, rather than just displaying the build status summary'
        });
        this._includePhaseDeps = this.defineFlagParameter({
            parameterLongName: '--include-phase-deps',
            description: 'If the selected projects are "unsafe" (missing some dependencies), add the minimal set of phase dependencies. For example, ' +
                `"--from A" normally might include the "_phase:test" phase for A's dependencies, even though changes to A can't break those tests. ` +
                `Using "--impacted-by A --include-phase-deps" avoids that work by performing "_phase:test" only for downstream projects.`
        });
        this._changedProjectsOnlyParameter = this._isIncrementalBuildAllowed
            ? this.defineFlagParameter({
                parameterLongName: '--changed-projects-only',
                parameterShortName: '-c',
                description: 'Normally the incremental build logic will rebuild changed projects as well as' +
                    ' any projects that directly or indirectly depend on a changed project. Specify "--changed-projects-only"' +
                    ' to ignore dependent projects, only rebuilding those projects whose files were changed.' +
                    ' Note that this parameter is "unsafe"; it is up to the developer to ensure that the ignored projects' +
                    ' are okay to ignore.'
            })
            : undefined;
        this._ignoreHooksParameter = this.defineFlagParameter({
            parameterLongName: '--ignore-hooks',
            description: `Skips execution of the "eventHooks" scripts defined in ${RushConstants_1.RushConstants.rushJsonFilename}. ` +
                'Make sure you know what you are skipping.'
        });
        // Only define the parameter if it has an effect.
        this._watchParameter =
            this._watchPhases.size > 0 && !this._alwaysWatch
                ? this.defineFlagParameter({
                    parameterLongName: '--watch',
                    description: `Starts a file watcher after initial execution finishes. Will run the following phases on affected projects: ${Array.from(this._watchPhases, (phase) => phase.name).join(', ')}`
                })
                : undefined;
        // If `this._alwaysInstall === undefined`, Rush does not define the parameter
        // but a repository may still define a custom parameter with the same name.
        this._installParameter =
            this._alwaysInstall === false
                ? this.defineFlagParameter({
                    parameterLongName: '--install',
                    description: 'Normally a phased command expects "rush install" to have been manually run first. If this flag is specified, ' +
                        'Rush will automatically perform an install before processing the current command.'
                })
                : undefined;
        this._variantParameter =
            this._alwaysInstall !== undefined ? this.defineStringParameter(Variants_1.VARIANT_PARAMETER) : undefined;
        const isIpcSupported = this._watchPhases.size > 0 &&
            !!this.rushConfiguration.experimentsConfiguration.configuration.useIPCScriptsInWatchMode;
        this._noIPCParameter = isIpcSupported
            ? this.defineFlagParameter({
                parameterLongName: '--no-ipc',
                description: 'Disables the IPC feature for the current command (if applicable to selected operations). Operations will not look for a ":ipc" suffixed script.' +
                    'This feature only applies in watch mode and is enabled by default.'
            })
            : undefined;
        this._nodeDiagnosticDirParameter = this.defineStringParameter({
            parameterLongName: '--node-diagnostic-dir',
            argumentName: 'DIRECTORY',
            description: 'Specifies the directory where Node.js diagnostic reports will be written. ' +
                'This directory will contain a subdirectory for each project and phase.'
        });
        this._debugBuildCacheIdsParameter = this.defineFlagParameter({
            parameterLongName: '--debug-build-cache-ids',
            description: 'Logs information about the components of the build cache ids for individual operations. This is useful for debugging the incremental build logic.'
        });
        this.defineScriptParameters();
        // Associate parameters with their respective phases
        (0, associateParametersByPhase_1.associateParametersByPhase)(this.customParameters, this._knownPhases);
    }
    async runAsync() {
        var _a, _b, _c, _d, _e, _f;
        const stopwatch = Stopwatch_1.Stopwatch.start();
        if (this._alwaysInstall || ((_a = this._installParameter) === null || _a === void 0 ? void 0 : _a.value)) {
            await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:install`, async () => {
                const { doBasicInstallAsync } = await Promise.resolve().then(() => __importStar(require(
                /* webpackChunkName: 'doBasicInstallAsync' */
                '../../logic/installManager/doBasicInstallAsync')));
                const variant = await (0, Variants_1.getVariantAsync)(this._variantParameter, this.rushConfiguration, true);
                await doBasicInstallAsync({
                    terminal: this._terminal,
                    rushConfiguration: this.rushConfiguration,
                    rushGlobalFolder: this.rushGlobalFolder,
                    isDebug: this.parser.isDebug,
                    variant,
                    beforeInstallAsync: (subspace) => this.rushSession.hooks.beforeInstall.promise(this, subspace, variant),
                    afterInstallAsync: (subspace) => this.rushSession.hooks.afterInstall.promise(this, subspace, variant),
                    // Eventually we may want to allow a subspace to be selected here
                    subspace: this.rushConfiguration.defaultSubspace
                });
            });
        }
        if (!this._runsBeforeInstall) {
            await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:checkInstallFlag`, async () => {
                // TODO: Replace with last-install.flag when "rush link" and "rush unlink" are removed
                const lastLinkFlag = new FlagFile_1.FlagFile(this.rushConfiguration.defaultSubspace.getSubspaceTempFolderPath(), RushConstants_1.RushConstants.lastLinkFlagFilename, {});
                // Only check for a valid link flag when subspaces is not enabled
                if (!(await lastLinkFlag.isValidAsync()) && !this.rushConfiguration.subspacesFeatureEnabled) {
                    const useWorkspaces = this.rushConfiguration.pnpmOptions && this.rushConfiguration.pnpmOptions.useWorkspaces;
                    if (useWorkspaces) {
                        throw new Error('Link flag invalid.\nDid you run "rush install" or "rush update"?');
                    }
                    else {
                        throw new Error('Link flag invalid.\nDid you run "rush link"?');
                    }
                }
            });
        }
        (0, performance_1.measureFn)(`${PERF_PREFIX}:doBeforeTask`, () => this._doBeforeTask());
        const hooks = this.hooks;
        const terminal = this._terminal;
        // if this is parallelizable, then use the value from the flag (undefined or a number),
        // if parallelism is not enabled, then restrict to 1 core
        const parallelism = this._enableParallelism
            ? (0, ParseParallelism_1.parseParallelism)((_b = this._parallelismParameter) === null || _b === void 0 ? void 0 : _b.value)
            : 1;
        const includePhaseDeps = (_d = (_c = this._includePhaseDeps) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : false;
        await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:applyStandardPlugins`, async () => {
            var _a, _b;
            // Generates the default operation graph
            new PhasedOperationPlugin_1.PhasedOperationPlugin().apply(hooks);
            // Splices in sharded phases to the operation graph.
            new ShardedPhaseOperationPlugin_1.ShardedPhasedOperationPlugin().apply(hooks);
            // Applies the Shell Operation Runner to selected operations
            new ShellOperationRunnerPlugin_1.ShellOperationRunnerPlugin().apply(hooks);
            new WeightedOperationPlugin_1.WeightedOperationPlugin().apply(hooks);
            new ValidateOperationsPlugin_1.ValidateOperationsPlugin(terminal).apply(hooks);
            // Forward ignored parameters to child processes as an environment variable
            new IgnoredParametersPlugin_1.IgnoredParametersPlugin().apply(hooks);
            const showTimeline = (_b = (_a = this._timelineParameter) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : false;
            if (showTimeline) {
                const { ConsoleTimelinePlugin } = await Promise.resolve().then(() => __importStar(require(
                /* webpackChunkName: 'ConsoleTimelinePlugin' */
                '../../logic/operations/ConsoleTimelinePlugin')));
                new ConsoleTimelinePlugin(terminal).apply(this.hooks);
            }
            const diagnosticDir = this._nodeDiagnosticDirParameter.value;
            if (diagnosticDir) {
                new NodeDiagnosticDirPlugin_1.NodeDiagnosticDirPlugin({
                    diagnosticDir
                }).apply(this.hooks);
            }
            // Enable the standard summary
            new OperationResultSummarizerPlugin_1.OperationResultSummarizerPlugin(terminal).apply(this.hooks);
        });
        const { hooks: sessionHooks } = this.rushSession;
        if (sessionHooks.runAnyPhasedCommand.isUsed()) {
            await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:runAnyPhasedCommand`, async () => {
                // Avoid the cost of compiling the hook if it wasn't tapped.
                await sessionHooks.runAnyPhasedCommand.promise(this);
            });
        }
        const hookForAction = sessionHooks.runPhasedCommand.get(this.actionName);
        if (hookForAction) {
            await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:runPhasedCommand`, async () => {
                // Run the more specific hook for a command with this name after the general hook
                await hookForAction.promise(this);
            });
        }
        const isQuietMode = !this._verboseParameter.value;
        const changedProjectsOnly = !!((_e = this._changedProjectsOnlyParameter) === null || _e === void 0 ? void 0 : _e.value);
        this._changedProjectsOnly = changedProjectsOnly;
        let buildCacheConfiguration;
        let cobuildConfiguration;
        if (!this._disableBuildCache) {
            await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:configureBuildCache`, async () => {
                [buildCacheConfiguration, cobuildConfiguration] = await Promise.all([
                    BuildCacheConfiguration_1.BuildCacheConfiguration.tryLoadAsync(terminal, this.rushConfiguration, this.rushSession),
                    CobuildConfiguration_1.CobuildConfiguration.tryLoadAsync(terminal, this.rushConfiguration, this.rushSession)
                ]);
                if (cobuildConfiguration) {
                    await cobuildConfiguration.createLockProviderAsync(terminal);
                }
            });
        }
        try {
            const projectSelection = await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:getSelectedProjects`, () => this._selectionParameters.getSelectedProjectsAsync(terminal));
            if (!projectSelection.size) {
                terminal.writeLine(terminal_1.Colorize.yellow(`The command line selection parameters did not match any projects.`));
                return;
            }
            const customParametersByName = new Map();
            for (const [configParameter, parserParameter] of this.customParameters) {
                customParametersByName.set(configParameter.longName, parserParameter);
            }
            const isWatch = ((_f = this._watchParameter) === null || _f === void 0 ? void 0 : _f.value) || this._alwaysWatch;
            await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:applySituationalPlugins`, async () => {
                var _a, _b, _c, _d;
                if (isWatch && ((_a = this._noIPCParameter) === null || _a === void 0 ? void 0 : _a.value) === false) {
                    new (await Promise.resolve().then(() => __importStar(require(
                    /* webpackChunkName: 'IPCOperationRunnerPlugin' */ '../../logic/operations/IPCOperationRunnerPlugin')))).IPCOperationRunnerPlugin().apply(this.hooks);
                }
                if (buildCacheConfiguration === null || buildCacheConfiguration === void 0 ? void 0 : buildCacheConfiguration.buildCacheEnabled) {
                    terminal.writeVerboseLine(`Incremental strategy: cache restoration`);
                    new CacheableOperationPlugin_1.CacheableOperationPlugin({
                        allowWarningsInSuccessfulBuild: !!this.rushConfiguration.experimentsConfiguration.configuration
                            .buildCacheWithAllowWarningsInSuccessfulBuild,
                        buildCacheConfiguration,
                        cobuildConfiguration,
                        terminal
                    }).apply(this.hooks);
                    if (this._debugBuildCacheIdsParameter.value) {
                        new DebugHashesPlugin_1.DebugHashesPlugin(terminal).apply(this.hooks);
                    }
                }
                else if (!this._disableBuildCache) {
                    terminal.writeVerboseLine(`Incremental strategy: output preservation`);
                    // Explicitly disabling the build cache also disables legacy skip detection.
                    new LegacySkipPlugin_1.LegacySkipPlugin({
                        allowWarningsInSuccessfulBuild: this.rushConfiguration.experimentsConfiguration.configuration
                            .buildSkipWithAllowWarningsInSuccessfulBuild,
                        terminal,
                        changedProjectsOnly,
                        isIncrementalBuildAllowed: this._isIncrementalBuildAllowed
                    }).apply(this.hooks);
                }
                else {
                    terminal.writeVerboseLine(`Incremental strategy: none (full rebuild)`);
                }
                const showBuildPlan = (_c = (_b = this._cobuildPlanParameter) === null || _b === void 0 ? void 0 : _b.value) !== null && _c !== void 0 ? _c : false;
                if (showBuildPlan) {
                    if (!(buildCacheConfiguration === null || buildCacheConfiguration === void 0 ? void 0 : buildCacheConfiguration.buildCacheEnabled)) {
                        throw new Error('You must have build cache enabled to use this option.');
                    }
                    const { BuildPlanPlugin } = await Promise.resolve().then(() => __importStar(require('../../logic/operations/BuildPlanPlugin')));
                    new BuildPlanPlugin(terminal).apply(this.hooks);
                }
                const { configuration: experiments } = this.rushConfiguration.experimentsConfiguration;
                if (((_d = this.rushConfiguration) === null || _d === void 0 ? void 0 : _d.isPnpm) && (experiments === null || experiments === void 0 ? void 0 : experiments.usePnpmSyncForInjectedDependencies)) {
                    const { PnpmSyncCopyOperationPlugin } = await Promise.resolve().then(() => __importStar(require('../../logic/operations/PnpmSyncCopyOperationPlugin')));
                    new PnpmSyncCopyOperationPlugin(terminal).apply(this.hooks);
                }
            });
            const relevantProjects = Selection_1.Selection.expandAllDependencies(projectSelection);
            const projectConfigurations = this
                ._runsBeforeInstall
                ? new Map()
                : await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:loadProjectConfigurations`, () => RushProjectConfiguration_1.RushProjectConfiguration.tryLoadForProjectsAsync(relevantProjects, terminal));
            const initialCreateOperationsContext = {
                buildCacheConfiguration,
                changedProjectsOnly,
                cobuildConfiguration,
                customParameters: customParametersByName,
                isIncrementalBuildAllowed: this._isIncrementalBuildAllowed,
                isInitial: true,
                isWatch,
                rushConfiguration: this.rushConfiguration,
                parallelism,
                phaseOriginal: new Set(this._originalPhases),
                phaseSelection: new Set(this._initialPhases),
                includePhaseDeps,
                projectSelection,
                projectConfigurations,
                projectsInUnknownState: projectSelection
            };
            const executionManagerOptions = {
                quietMode: isQuietMode,
                debugMode: this.parser.isDebug,
                parallelism,
                allowOversubscription: this._allowOversubscription,
                beforeExecuteOperationAsync: async (record) => {
                    return await this.hooks.beforeExecuteOperation.promise(record);
                },
                afterExecuteOperationAsync: async (record) => {
                    await this.hooks.afterExecuteOperation.promise(record);
                },
                createEnvironmentForOperation: this.hooks.createEnvironmentForOperation.isUsed()
                    ? (record) => {
                        return this.hooks.createEnvironmentForOperation.call({ ...process.env }, record);
                    }
                    : undefined,
                onOperationStatusChangedAsync: (record) => {
                    this.hooks.onOperationStatusChanged.call(record);
                }
            };
            const initialInternalOptions = {
                initialCreateOperationsContext,
                executionManagerOptions,
                stopwatch,
                terminal
            };
            const internalOptions = await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:runInitialPhases`, () => this._runInitialPhasesAsync(initialInternalOptions));
            if (isWatch) {
                if (buildCacheConfiguration) {
                    // Cache writes are not supported during watch mode, only reads.
                    buildCacheConfiguration.cacheWriteEnabled = false;
                }
                await this._runWatchPhasesAsync(internalOptions);
                terminal.writeDebugLine(`Watch mode exited.`);
            }
        }
        finally {
            if (cobuildConfiguration) {
                await cobuildConfiguration.destroyLockProviderAsync();
            }
        }
    }
    async _runInitialPhasesAsync(options) {
        const { initialCreateOperationsContext, executionManagerOptions: partialExecutionManagerOptions, stopwatch, terminal } = options;
        const { projectConfigurations } = initialCreateOperationsContext;
        const { projectSelection } = initialCreateOperationsContext;
        const operations = await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:createOperations`, () => this.hooks.createOperations.promise(new Set(), initialCreateOperationsContext));
        const [getInputsSnapshotAsync, initialSnapshot] = await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:analyzeRepoState`, async () => {
            terminal.write('Analyzing repo state... ');
            const repoStateStopwatch = new Stopwatch_1.Stopwatch();
            repoStateStopwatch.start();
            const analyzer = new ProjectChangeAnalyzer_1.ProjectChangeAnalyzer(this.rushConfiguration);
            const innerGetInputsSnapshotAsync = await analyzer._tryGetSnapshotProviderAsync(projectConfigurations, terminal, 
            // We need to include all dependencies, otherwise build cache id calculation will be incorrect
            Selection_1.Selection.expandAllDependencies(projectSelection));
            const innerInitialSnapshot = innerGetInputsSnapshotAsync
                ? await innerGetInputsSnapshotAsync()
                : undefined;
            repoStateStopwatch.stop();
            terminal.writeLine(`DONE (${repoStateStopwatch.toString()})`);
            terminal.writeLine();
            return [innerGetInputsSnapshotAsync, innerInitialSnapshot];
        });
        const abortController = (this._executionAbortController = new AbortController());
        const initialExecuteOperationsContext = {
            ...initialCreateOperationsContext,
            inputsSnapshot: initialSnapshot,
            abortController
        };
        const executionManagerOptions = {
            ...partialExecutionManagerOptions,
            inputsSnapshot: initialSnapshot,
            beforeExecuteOperationsAsync: async (records) => {
                await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:beforeExecuteOperations`, () => this.hooks.beforeExecuteOperations.promise(records, initialExecuteOperationsContext));
            }
        };
        const initialOptions = {
            executeOperationsContext: initialExecuteOperationsContext,
            ignoreHooks: false,
            operations,
            stopwatch,
            executionManagerOptions,
            terminal
        };
        await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:executeOperations`, () => this._executeOperationsAsync(initialOptions));
        return {
            ...options,
            executionManagerOptions,
            getInputsSnapshotAsync,
            initialSnapshot
        };
    }
    _registerWatchModeInterface(projectWatcher) {
        const buildOnceKey = 'b';
        const changedProjectsOnlyKey = 'c';
        const invalidateKey = 'i';
        const quitKey = 'q';
        const abortKey = 'a';
        const toggleWatcherKey = 'w';
        const shutdownProcessesKey = 'x';
        const terminal = this._terminal;
        projectWatcher.setPromptGenerator((isPaused) => {
            var _a;
            const promptLines = [
                `  Press <${quitKey}> to gracefully exit.`,
                `  Press <${abortKey}> to abort queued operations. Any that have started will finish.`,
                `  Press <${toggleWatcherKey}> to ${isPaused ? 'resume' : 'pause'}.`,
                `  Press <${invalidateKey}> to invalidate all projects.`,
                `  Press <${changedProjectsOnlyKey}> to ${this._changedProjectsOnly ? 'disable' : 'enable'} changed-projects-only mode (${this._changedProjectsOnly ? 'ENABLED' : 'DISABLED'}).`
            ];
            if (isPaused) {
                promptLines.push(`  Press <${buildOnceKey}> to build once.`);
            }
            if (((_a = this._noIPCParameter) === null || _a === void 0 ? void 0 : _a.value) === false) {
                promptLines.push(`  Press <${shutdownProcessesKey}> to reset child processes.`);
            }
            return promptLines;
        });
        const onKeyPress = (key) => {
            var _a;
            switch (key) {
                case quitKey:
                    terminal.writeLine(`Exiting watch mode and aborting any scheduled work...`);
                    process.stdin.setRawMode(false);
                    process.stdin.off('data', onKeyPress);
                    process.stdin.unref();
                    this.sessionAbortController.abort();
                    break;
                case abortKey:
                    terminal.writeLine(`Aborting current iteration...`);
                    (_a = this._executionAbortController) === null || _a === void 0 ? void 0 : _a.abort();
                    break;
                case toggleWatcherKey:
                    if (projectWatcher.isPaused) {
                        projectWatcher.resume();
                    }
                    else {
                        projectWatcher.pause();
                    }
                    break;
                case buildOnceKey:
                    if (projectWatcher.isPaused) {
                        projectWatcher.clearStatus();
                        terminal.writeLine(`Building once...`);
                        projectWatcher.resume();
                        projectWatcher.pause();
                    }
                    break;
                case invalidateKey:
                    projectWatcher.clearStatus();
                    terminal.writeLine(`Invalidating all operations...`);
                    projectWatcher.invalidateAll('manual trigger');
                    if (!projectWatcher.isPaused) {
                        projectWatcher.resume();
                    }
                    break;
                case changedProjectsOnlyKey:
                    this._changedProjectsOnly = !this._changedProjectsOnly;
                    projectWatcher.rerenderStatus();
                    break;
                case shutdownProcessesKey:
                    projectWatcher.clearStatus();
                    terminal.writeLine(`Shutting down long-lived child processes...`);
                    // TODO: Inject this promise into the execution queue somewhere so that it gets waited on between runs
                    void this.hooks.shutdownAsync.promise();
                    break;
                case '\u0003':
                    process.stdin.setRawMode(false);
                    process.stdin.off('data', onKeyPress);
                    process.stdin.unref();
                    this.sessionAbortController.abort();
                    process.kill(process.pid, 'SIGINT');
                    break;
            }
        };
        process.stdin.setRawMode(true);
        process.stdin.resume();
        process.stdin.setEncoding('utf8');
        process.stdin.on('data', onKeyPress);
    }
    /**
     * Runs the command in watch mode. Fundamentally is a simple loop:
     * 1) Wait for a change to one or more projects in the selection
     * 2) Invoke the command on the changed projects, and, if applicable, impacted projects
     *    Uses the same algorithm as --impacted-by
     * 3) Goto (1)
     */
    async _runWatchPhasesAsync(options) {
        const { getInputsSnapshotAsync, initialSnapshot, initialCreateOperationsContext, executionManagerOptions, stopwatch, terminal } = options;
        const phaseOriginal = new Set(this._watchPhases);
        const phaseSelection = new Set(this._watchPhases);
        const { projectSelection: projectsToWatch } = initialCreateOperationsContext;
        if (!getInputsSnapshotAsync || !initialSnapshot) {
            terminal.writeErrorLine(`Cannot watch for changes if the Rush repo is not in a Git repository, exiting.`);
            throw new node_core_library_1.AlreadyReportedError();
        }
        // Use async import so that we don't pay the cost for sync builds
        const { ProjectWatcher } = await Promise.resolve().then(() => __importStar(require(
        /* webpackChunkName: 'ProjectWatcher' */
        '../../logic/ProjectWatcher')));
        const sessionAbortController = this.sessionAbortController;
        const abortSignal = sessionAbortController.signal;
        const projectWatcher = new ProjectWatcher({
            getInputsSnapshotAsync,
            initialSnapshot,
            debounceMs: this._watchDebounceMs,
            rushConfiguration: this.rushConfiguration,
            projectsToWatch,
            abortSignal,
            terminal
        });
        // Ensure process.stdin allows interactivity before using TTY-only APIs
        if (process.stdin.isTTY) {
            this._registerWatchModeInterface(projectWatcher);
        }
        const onWaitingForChanges = () => {
            // Allow plugins to display their own messages when waiting for changes.
            this.hooks.waitingForChanges.call();
            // Report so that the developer can always see that it is in watch mode as the latest console line.
            terminal.writeLine(`Watching for changes to ${projectsToWatch.size} ${projectsToWatch.size === 1 ? 'project' : 'projects'}. Press Ctrl+C to exit.`);
        };
        function invalidateOperation(operation, reason) {
            const { associatedProject } = operation;
            // Since ProjectWatcher only tracks entire projects, widen the operation to its project
            // Revisit when migrating to @rushstack/operation-graph and we have a long-lived operation graph
            projectWatcher.invalidateProject(associatedProject, `${operation.name} (${reason})`);
        }
        // Loop until Ctrl+C
        while (!abortSignal.aborted) {
            // On the initial invocation, this promise will return immediately with the full set of projects
            const { changedProjects, inputsSnapshot: state } = await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:waitForChanges`, () => projectWatcher.waitForChangeAsync(onWaitingForChanges));
            if (abortSignal.aborted) {
                return;
            }
            if (stopwatch.state === Stopwatch_1.StopwatchState.Stopped) {
                // Clear and reset the stopwatch so that we only report time from a single execution at a time
                stopwatch.reset();
                stopwatch.start();
            }
            terminal.writeLine(`Detected changes in ${changedProjects.size} project${changedProjects.size === 1 ? '' : 's'}:`);
            const names = [...changedProjects].map((x) => x.packageName).sort();
            for (const name of names) {
                terminal.writeLine(`    ${terminal_1.Colorize.cyan(name)}`);
            }
            const initialAbortController = (this._executionAbortController =
                new AbortController());
            // Account for consumer relationships
            const executeOperationsContext = {
                ...initialCreateOperationsContext,
                abortController: initialAbortController,
                changedProjectsOnly: !!this._changedProjectsOnly,
                isInitial: false,
                inputsSnapshot: state,
                projectsInUnknownState: changedProjects,
                phaseOriginal,
                phaseSelection,
                invalidateOperation
            };
            const operations = await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:createOperations`, () => this.hooks.createOperations.promise(new Set(), executeOperationsContext));
            const executeOptions = {
                executeOperationsContext,
                // For now, don't run pre-build or post-build in watch mode
                ignoreHooks: true,
                operations,
                stopwatch,
                executionManagerOptions: {
                    ...executionManagerOptions,
                    inputsSnapshot: state,
                    beforeExecuteOperationsAsync: async (records) => {
                        await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:beforeExecuteOperations`, () => this.hooks.beforeExecuteOperations.promise(records, executeOperationsContext));
                    }
                },
                terminal
            };
            try {
                // Delegate the the underlying command, for only the projects that need reprocessing
                await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:executeOperations`, () => this._executeOperationsAsync(executeOptions));
            }
            catch (err) {
                // In watch mode, we want to rebuild even if the original build failed.
                if (!(err instanceof node_core_library_1.AlreadyReportedError)) {
                    throw err;
                }
            }
        }
    }
    /**
     * Runs a set of operations and reports the results.
     */
    async _executeOperationsAsync(options) {
        const { executeOperationsContext, executionManagerOptions, ignoreHooks, operations, stopwatch, terminal } = options;
        const executionManager = new OperationExecutionManager_1.OperationExecutionManager(operations, executionManagerOptions);
        const { isInitial, isWatch, abortController, invalidateOperation } = executeOperationsContext;
        let success = false;
        let result;
        try {
            const definiteResult = await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:executeOperationsInner`, () => executionManager.executeAsync(abortController));
            success = definiteResult.status === OperationStatus_1.OperationStatus.Success;
            result = definiteResult;
            await (0, performance_1.measureAsyncFn)(`${PERF_PREFIX}:afterExecuteOperations`, () => this.hooks.afterExecuteOperations.promise(definiteResult, executeOperationsContext));
            stopwatch.stop();
            const message = `rush ${this.actionName} (${stopwatch.toString()})`;
            if (result.status === OperationStatus_1.OperationStatus.Success) {
                terminal.writeLine(terminal_1.Colorize.green(message));
            }
            else {
                terminal.writeLine(message);
            }
        }
        catch (error) {
            success = false;
            stopwatch.stop();
            if (error instanceof node_core_library_1.AlreadyReportedError) {
                terminal.writeLine(`rush ${this.actionName} (${stopwatch.toString()})`);
            }
            else {
                if (error && error.message) {
                    if (this.parser.isDebug) {
                        terminal.writeErrorLine('Error: ' + error.stack);
                    }
                    else {
                        terminal.writeErrorLine('Error: ' + error.message);
                    }
                }
                terminal.writeErrorLine(terminal_1.Colorize.red(`rush ${this.actionName} - Errors! (${stopwatch.toString()})`));
            }
        }
        this._executionAbortController = undefined;
        if (invalidateOperation) {
            const operationResults = result === null || result === void 0 ? void 0 : result.operationResults;
            if (operationResults) {
                for (const [operation, { status }] of operationResults) {
                    if (status === OperationStatus_1.OperationStatus.Aborted) {
                        invalidateOperation(operation, 'aborted');
                    }
                }
            }
        }
        if (!ignoreHooks) {
            (0, performance_1.measureFn)(`${PERF_PREFIX}:doAfterTask`, () => this._doAfterTask());
        }
        if (this.parser.telemetry) {
            const logEntry = (0, performance_1.measureFn)(`${PERF_PREFIX}:prepareTelemetry`, () => {
                var _a;
                const jsonOperationResults = {};
                const extraData = {
                    // Fields preserved across the command invocation
                    ...this._selectionParameters.getTelemetry(),
                    ...this.getParameterStringMap(),
                    isWatch,
                    // Fields specific to the current operation set
                    isInitial,
                    countAll: 0,
                    countSuccess: 0,
                    countSuccessWithWarnings: 0,
                    countFailure: 0,
                    countBlocked: 0,
                    countFromCache: 0,
                    countSkipped: 0,
                    countNoOp: 0
                };
                const { _changedProjectsOnlyParameter: changedProjectsOnlyParameter } = this;
                if (changedProjectsOnlyParameter) {
                    // Overwrite this value since we allow changing it at runtime.
                    extraData[(_a = changedProjectsOnlyParameter.scopedLongName) !== null && _a !== void 0 ? _a : changedProjectsOnlyParameter.longName] =
                        this._changedProjectsOnly;
                }
                if (result) {
                    const { operationResults } = result;
                    const nonSilentDependenciesByOperation = new Map();
                    function getNonSilentDependencies(operation) {
                        let realDependencies = nonSilentDependenciesByOperation.get(operation);
                        if (!realDependencies) {
                            realDependencies = new Set();
                            nonSilentDependenciesByOperation.set(operation, realDependencies);
                            for (const dependency of operation.dependencies) {
                                const dependencyRecord = operationResults.get(dependency);
                                if (dependencyRecord === null || dependencyRecord === void 0 ? void 0 : dependencyRecord.silent) {
                                    for (const deepDependency of getNonSilentDependencies(dependency)) {
                                        realDependencies.add(deepDependency);
                                    }
                                }
                                else {
                                    realDependencies.add(dependency.name);
                                }
                            }
                        }
                        return realDependencies;
                    }
                    for (const [operation, operationResult] of operationResults) {
                        if (operationResult.silent) {
                            // Architectural operation. Ignore.
                            continue;
                        }
                        const { _operationMetadataManager: operationMetadataManager } = operationResult;
                        const { startTime, endTime } = operationResult.stopwatch;
                        jsonOperationResults[operation.name] = {
                            startTimestampMs: startTime,
                            endTimestampMs: endTime,
                            nonCachedDurationMs: operationResult.nonCachedDurationMs,
                            wasExecutedOnThisMachine: (operationMetadataManager === null || operationMetadataManager === void 0 ? void 0 : operationMetadataManager.wasCobuilt) !== true,
                            result: operationResult.status,
                            dependencies: Array.from(getNonSilentDependencies(operation)).sort()
                        };
                        extraData.countAll++;
                        switch (operationResult.status) {
                            case OperationStatus_1.OperationStatus.Success:
                                extraData.countSuccess++;
                                break;
                            case OperationStatus_1.OperationStatus.SuccessWithWarning:
                                extraData.countSuccessWithWarnings++;
                                break;
                            case OperationStatus_1.OperationStatus.Failure:
                                extraData.countFailure++;
                                break;
                            case OperationStatus_1.OperationStatus.Blocked:
                                extraData.countBlocked++;
                                break;
                            case OperationStatus_1.OperationStatus.FromCache:
                                extraData.countFromCache++;
                                break;
                            case OperationStatus_1.OperationStatus.Skipped:
                                extraData.countSkipped++;
                                break;
                            case OperationStatus_1.OperationStatus.NoOp:
                                extraData.countNoOp++;
                                break;
                            default:
                                // Do nothing.
                                break;
                        }
                    }
                }
                const innerLogEntry = {
                    name: this.actionName,
                    durationInSeconds: stopwatch.duration,
                    result: success ? 'Succeeded' : 'Failed',
                    extraData,
                    operationResults: jsonOperationResults
                };
                return innerLogEntry;
            });
            (0, performance_1.measureFn)(`${PERF_PREFIX}:beforeLog`, () => this.hooks.beforeLog.call(logEntry));
            this.parser.telemetry.log(logEntry);
            this.parser.flushTelemetry();
        }
        if (!success && !isWatch) {
            throw new node_core_library_1.AlreadyReportedError();
        }
    }
    _doBeforeTask() {
        if (this.actionName !== RushConstants_1.RushConstants.buildCommandName &&
            this.actionName !== RushConstants_1.RushConstants.rebuildCommandName) {
            // Only collects information for built-in commands like build or rebuild.
            return;
        }
        SetupChecks_1.SetupChecks.validate(this.rushConfiguration);
        this.eventHooksManager.handle(EventHooks_1.Event.preRushBuild, this.parser.isDebug, this._ignoreHooksParameter.value);
    }
    _doAfterTask() {
        if (this.actionName !== RushConstants_1.RushConstants.buildCommandName &&
            this.actionName !== RushConstants_1.RushConstants.rebuildCommandName) {
            // Only collects information for built-in commands like build or rebuild.
            return;
        }
        this.eventHooksManager.handle(EventHooks_1.Event.postRushBuild, this.parser.isDebug, this._ignoreHooksParameter.value);
    }
}
exports.PhasedScriptAction = PhasedScriptAction;
//# sourceMappingURL=PhasedScriptAction.js.map