// 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 { FileSystem, JsonFile, AlreadyReportedError, Text } from '@rushstack/node-core-library';
import { Colorize } from '@rushstack/terminal';
import { BaseScriptAction } from './BaseScriptAction';
import { Utilities } from '../../utilities/Utilities';
import { Stopwatch } from '../../utilities/Stopwatch';
import { Autoinstaller } from '../../logic/Autoinstaller';
import { measureAsyncFn } from '../../utilities/performance';
/**
 * This class implements custom commands that are run once globally for the entire repo
 * (versus bulk commands, which run separately for each project).  The action executes
 * a user-defined script file.
 *
 * @remarks
 * Bulk commands can be defined via common/config/command-line.json.  Rush's predefined "build"
 * and "rebuild" commands are also modeled as bulk commands, because they essentially just
 * invoke scripts from package.json in the same way as a custom command.
 */
export class GlobalScriptAction extends BaseScriptAction {
    constructor(options) {
        super(options);
        this._shellCommand = options.shellCommand;
        this._autoinstallerName = options.autoinstallerName || '';
        if (this._autoinstallerName) {
            Autoinstaller.validateName(this._autoinstallerName);
            // Example: .../common/autoinstallers/my-task
            this._autoinstallerFullPath = path.join(this.rushConfiguration.commonAutoinstallersFolder, this._autoinstallerName);
            if (!FileSystem.exists(this._autoinstallerFullPath)) {
                throw new Error(`The custom command "${this.actionName}" specifies an "autoinstallerName" setting` +
                    ' but the path does not exist: ' +
                    this._autoinstallerFullPath);
            }
            // Example: .../common/autoinstallers/my-task/package.json
            const packageJsonPath = path.join(this._autoinstallerFullPath, 'package.json');
            if (!FileSystem.exists(packageJsonPath)) {
                throw new Error(`The custom command "${this.actionName}" specifies an "autoinstallerName" setting` +
                    ` whose package.json file was not found: ` +
                    packageJsonPath);
            }
            const packageJson = JsonFile.load(packageJsonPath);
            if (packageJson.name !== this._autoinstallerName) {
                throw new Error(`The custom command "${this.actionName}" specifies an "autoinstallerName" setting,` +
                    ` but the package.json file's "name" field is not "${this._autoinstallerName}": ` +
                    packageJsonPath);
            }
        }
        else {
            this._autoinstallerFullPath = '';
        }
        this.defineScriptParameters();
    }
    async _prepareAutoinstallerNameAsync() {
        const autoInstaller = new Autoinstaller({
            autoinstallerName: this._autoinstallerName,
            rushConfiguration: this.rushConfiguration,
            rushGlobalFolder: this.rushGlobalFolder
        });
        await autoInstaller.prepareAsync();
    }
    async runAsync() {
        var _a, _b;
        const { hooks: sessionHooks } = this.rushSession;
        if (sessionHooks.runAnyGlobalCustomCommand.isUsed()) {
            // Avoid the cost of compiling the hook if it wasn't tapped.
            await sessionHooks.runAnyGlobalCustomCommand.promise(this);
        }
        const hookForAction = sessionHooks.runGlobalCustomCommand.get(this.actionName);
        if (hookForAction) {
            // Run the more specific hook for a command with this name after the general hook
            await hookForAction.promise(this);
        }
        const additionalPathFolders = ((_a = this.commandLineConfiguration) === null || _a === void 0 ? void 0 : _a.additionalPathFolders.slice()) || [];
        if (this._autoinstallerName) {
            await measureAsyncFn('rush:globalScriptAction:prepareAutoinstaller', () => this._prepareAutoinstallerNameAsync());
            const autoinstallerNameBinPath = path.join(this._autoinstallerFullPath, 'node_modules', '.bin');
            additionalPathFolders.push(autoinstallerNameBinPath);
        }
        // Collect all custom parameter values
        const customParameterValues = [];
        for (const tsCommandLineParameter of this.customParameters.values()) {
            tsCommandLineParameter.appendToArgList(customParameterValues);
        }
        for (let i = 0; i < customParameterValues.length; i++) {
            let customParameterValue = customParameterValues[i];
            customParameterValue = customParameterValue.replace(/"/g, '\\"');
            if (customParameterValue.indexOf(' ') >= 0) {
                customParameterValue = `"${customParameterValue}"`;
            }
            customParameterValues[i] = customParameterValue;
        }
        let shellCommand = this._shellCommand;
        if (customParameterValues.length > 0) {
            shellCommand += ' ' + customParameterValues.join(' ');
        }
        const shellCommandTokenContext = (_b = this.commandLineConfiguration) === null || _b === void 0 ? void 0 : _b.shellCommandTokenContext;
        if (shellCommandTokenContext) {
            shellCommand = this._expandShellCommandWithTokens(shellCommand, shellCommandTokenContext);
        }
        this._rejectAnyTokensInShellCommand(shellCommand, shellCommandTokenContext);
        const stopwatch = Stopwatch.start();
        const exitCode = Utilities.executeLifecycleCommand(shellCommand, {
            rushConfiguration: this.rushConfiguration,
            workingDirectory: this.rushConfiguration.rushJsonFolder,
            initCwd: this.rushConfiguration.commonTempFolder,
            handleOutput: false,
            environmentPathOptions: {
                includeRepoBin: true,
                additionalPathFolders: additionalPathFolders
            }
        });
        process.exitCode = exitCode;
        stopwatch.stop();
        if (this.parser.telemetry) {
            this.parser.telemetry.log({
                name: this.actionName,
                durationInSeconds: stopwatch.duration,
                result: exitCode > 0 ? 'Failed' : 'Succeeded',
                extraData: {
                    customParameterValue: customParameterValues.join(' ')
                }
            });
            this.parser.flushTelemetry();
        }
        if (exitCode > 0) {
            // eslint-disable-next-line no-console
            console.log('\n' + Colorize.red(`The script failed with exit code ${exitCode}`));
            throw new AlreadyReportedError();
        }
    }
    _expandShellCommandWithTokens(shellCommand, tokenContext) {
        let expandedShellCommand = shellCommand;
        for (const [token, tokenReplacement] of Object.entries(tokenContext)) {
            expandedShellCommand = Text.replaceAll(expandedShellCommand, `<${token}>`, tokenReplacement);
        }
        return expandedShellCommand;
    }
    _rejectAnyTokensInShellCommand(shellCommand, tokenContext) {
        if (shellCommand.indexOf('<') < 0 && shellCommand.indexOf('>') < 0) {
            return;
        }
        const tokenRegExp = /(\<[^<]*?\>)/;
        const match = tokenRegExp.exec(shellCommand);
        if (match) {
            throw new Error(`The "shellCommand" value contains an unrecognized token "${match[1]}".${tokenContext ? ` Available tokens are ${Object.keys(tokenContext).join(', ')}.` : ''}`);
        }
        throw new Error(`The "shellCommand" value contains extra token characters ("<" or ">"): ${shellCommand}`);
    }
}
//# sourceMappingURL=GlobalScriptAction.js.map