// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as os from 'node:os';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { existsSync } from 'node:fs';
import { readFile, unlink } from 'node:fs/promises';
import * as semver from 'semver';
import { pnpmSyncGetJsonVersion, pnpmSyncPrepareAsync } from 'pnpm-sync-lib';
import { FileSystem, JsonFile, PosixModeBits, NewlineKind, AlreadyReportedError, Path, Async } from '@rushstack/node-core-library';
import { PrintUtilities, Colorize } from '@rushstack/terminal';
import { ApprovedPackagesChecker } from '../ApprovedPackagesChecker';
import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration';
import { Git } from '../Git';
import { getCommonTempFlag } from '../../api/LastInstallFlag';
import { Rush } from '../../api/Rush';
import { RushConstants } from '../RushConstants';
import { ShrinkwrapFileFactory } from '../ShrinkwrapFileFactory';
import { Utilities } from '../../utilities/Utilities';
import { InstallHelpers } from '../installManager/InstallHelpers';
import * as PolicyValidator from '../policy/PolicyValidator';
import { SetupPackageRegistry } from '../setup/SetupPackageRegistry';
import { PnpmfileConfiguration } from '../pnpm/PnpmfileConfiguration';
import { isVariableSetInNpmrcFile } from '../../utilities/npmrcUtilities';
import { SubspacePnpmfileConfiguration } from '../pnpm/SubspacePnpmfileConfiguration';
import { ProjectImpactGraphGenerator } from '../ProjectImpactGraphGenerator';
import { FlagFile } from '../../api/FlagFile';
import { PnpmSyncUtilities } from '../../utilities/PnpmSyncUtilities';
import { HotlinkManager } from '../../utilities/HotlinkManager';
/**
 * Pnpm don't support --ignore-compatibility-db, so use --config.ignoreCompatibilityDb for now.
 */
export const pnpmIgnoreCompatibilityDbParameter = '--config.ignoreCompatibilityDb';
const pnpmCacheDirParameter = '--config.cacheDir';
const pnpmStateDirParameter = '--config.stateDir';
const gitLfsHooks = new Set(['post-checkout', 'post-commit', 'post-merge', 'pre-push']);
/**
 * This class implements common logic between "rush install" and "rush update".
 */
export class BaseInstallManager {
    constructor(rushConfiguration, rushGlobalFolder, purgeManager, options) {
        this._npmSetupValidated = false;
        this._syncNpmrcAlreadyCalled = false;
        this._terminal = options.terminal;
        this.rushConfiguration = rushConfiguration;
        this.rushGlobalFolder = rushGlobalFolder;
        this.installRecycler = purgeManager.commonTempFolderRecycler;
        this.options = options;
        this._commonTempLinkFlag = new FlagFile(options.subspace.getSubspaceTempFolderPath(), RushConstants.lastLinkFlagFilename, {});
        this.subspaceInstallFlags = new Map();
        if (rushConfiguration.subspacesFeatureEnabled) {
            for (const subspace of rushConfiguration.subspaces) {
                this.subspaceInstallFlags.set(subspace.subspaceName, getCommonTempFlag(rushConfiguration, subspace));
            }
        }
    }
    async doInstallAsync() {
        const { allowShrinkwrapUpdates, selectedProjects, pnpmFilterArgumentValues, resolutionOnly, variant } = this.options;
        const isFilteredInstall = pnpmFilterArgumentValues.length > 0;
        const useWorkspaces = this.rushConfiguration.pnpmOptions && this.rushConfiguration.pnpmOptions.useWorkspaces;
        // Prevent filtered installs when workspaces is disabled
        if (isFilteredInstall && !useWorkspaces) {
            // eslint-disable-next-line no-console
            console.log();
            // eslint-disable-next-line no-console
            console.log(Colorize.red('Project filtering arguments can only be used when running in a workspace environment. Run the ' +
                'command again without specifying these arguments.'));
            throw new AlreadyReportedError();
        }
        // Prevent update when using a filter, as modifications to the shrinkwrap shouldn't be saved
        if (allowShrinkwrapUpdates && isFilteredInstall) {
            // Allow partial update when there are subspace projects
            if (!this.rushConfiguration.subspacesFeatureEnabled) {
                // eslint-disable-next-line no-console
                console.log();
                // eslint-disable-next-line no-console
                console.log(Colorize.red('Project filtering arguments cannot be used when running "rush update". Run the command again ' +
                    'without specifying these arguments.'));
                throw new AlreadyReportedError();
            }
        }
        const subspace = this.options.subspace;
        const projectImpactGraphGenerator = this.rushConfiguration
            .experimentsConfiguration.configuration.generateProjectImpactGraphDuringRushUpdate
            ? new ProjectImpactGraphGenerator(this._terminal, this.rushConfiguration)
            : undefined;
        const { shrinkwrapIsUpToDate, npmrcHash, projectImpactGraphIsUpToDate, variantIsUpToDate } = await this.prepareAsync(subspace, variant, projectImpactGraphGenerator);
        if (this.options.checkOnly) {
            return;
        }
        // eslint-disable-next-line no-console
        console.log('\n' + Colorize.bold(`Checking installation in "${subspace.getSubspaceTempFolderPath()}"`));
        // This marker file indicates that the last "rush install" completed successfully.
        // Always perform a clean install if filter flags were provided. Additionally, if
        // "--purge" was specified, or if the last install was interrupted, then we will
        // need to perform a clean install.  Otherwise, we can do an incremental install.
        const commonTempInstallFlag = getCommonTempFlag(this.rushConfiguration, subspace, {
            npmrcHash: npmrcHash || '<NO NPMRC>'
        });
        if (isFilteredInstall && selectedProjects) {
            const selectedProjectNames = [];
            for (const { packageName } of selectedProjects) {
                selectedProjectNames.push(packageName);
            }
            selectedProjectNames.sort();
            // Get the projects involved in this filtered install
            commonTempInstallFlag.mergeFromObject({
                selectedProjectNames
            });
        }
        const optionsToIgnore = !this.rushConfiguration
            .experimentsConfiguration.configuration.cleanInstallAfterNpmrcChanges
            ? ['npmrcHash'] // If the "cleanInstallAfterNpmrcChanges" experiment is disabled, ignore the npmrcHash
            : undefined;
        const cleanInstall = !(await commonTempInstallFlag.checkValidAndReportStoreIssuesAsync({
            rushVerb: allowShrinkwrapUpdates ? 'update' : 'install',
            statePropertiesToIgnore: optionsToIgnore
        }));
        const hotlinkManager = HotlinkManager.loadFromRushConfiguration(this.rushConfiguration);
        const wasNodeModulesModifiedOutsideInstallation = await hotlinkManager.purgeLinksAsync(this._terminal, subspace.subspaceName);
        // Allow us to defer the file read until we need it
        const canSkipInstallAsync = async () => {
            // Based on timestamps, can we skip this install entirely?
            const outputStats = await FileSystem.getStatisticsAsync(commonTempInstallFlag.path);
            return this.canSkipInstallAsync(outputStats.mtime, subspace, variant);
        };
        if (resolutionOnly ||
            cleanInstall ||
            wasNodeModulesModifiedOutsideInstallation ||
            !variantIsUpToDate ||
            !shrinkwrapIsUpToDate ||
            !(await canSkipInstallAsync()) ||
            !projectImpactGraphIsUpToDate) {
            // eslint-disable-next-line no-console
            console.log();
            await this.validateNpmSetupAsync();
            if (!this.rushConfiguration.rushConfigurationJson.suppressRushIsPublicVersionCheck) {
                let publishedRelease;
                try {
                    publishedRelease = await this._checkIfReleaseIsPublishedAsync();
                }
                catch (_a) {
                    // If the user is working in an environment that can't reach the registry,
                    // don't bother them with errors.
                }
                if (publishedRelease === false) {
                    // eslint-disable-next-line no-console
                    console.log(Colorize.yellow('Warning: This release of the Rush tool was unpublished; it may be unstable.'));
                }
            }
            if (!resolutionOnly) {
                // Delete the successful install file to indicate the install transaction has started
                await commonTempInstallFlag.clearAsync();
                // Since we're going to be tampering with common/node_modules, delete the "rush link" flag file if it exists;
                // this ensures that a full "rush link" is required next time
                await this._commonTempLinkFlag.clearAsync();
            }
            // Give plugins an opportunity to act before invoking the installation process
            if (this.options.beforeInstallAsync !== undefined) {
                await this.options.beforeInstallAsync(subspace);
            }
            await Promise.all([
                // Perform the actual install
                this.installAsync(cleanInstall, subspace),
                // If allowed, generate the project impact graph
                allowShrinkwrapUpdates ? projectImpactGraphGenerator === null || projectImpactGraphGenerator === void 0 ? void 0 : projectImpactGraphGenerator.generateAsync() : undefined
            ]);
            if (this.options.allowShrinkwrapUpdates && !shrinkwrapIsUpToDate) {
                const shrinkwrapFilePath = subspace.getCommittedShrinkwrapFilePath(variant);
                const shrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({
                    packageManager: this.rushConfiguration.packageManager,
                    shrinkwrapFilePath,
                    subspaceHasNoProjects: subspace.getProjects().length === 0
                });
                shrinkwrapFile === null || shrinkwrapFile === void 0 ? void 0 : shrinkwrapFile.validateShrinkwrapAfterUpdate(this.rushConfiguration, subspace, this._terminal);
                // Copy (or delete) common\temp\pnpm-lock.yaml --> common\config\rush\pnpm-lock.yaml
                Utilities.syncFile(subspace.getTempShrinkwrapFilename(), shrinkwrapFilePath);
            }
            else {
                // TODO: Validate whether the package manager updated it in a nontrivial way
            }
            // Always update the state file if running "rush update"
            if (this.options.allowShrinkwrapUpdates) {
                if (subspace.getRepoState().refreshState(this.rushConfiguration, subspace, variant)) {
                    // eslint-disable-next-line no-console
                    console.log(Colorize.yellow(`${RushConstants.repoStateFilename} has been modified and must be committed to source control.`));
                }
            }
        }
        else {
            // eslint-disable-next-line no-console
            console.log('Installation is already up-to-date.');
        }
        const { configuration: experiments } = this.rushConfiguration.experimentsConfiguration;
        // if usePnpmSyncForInjectedDependencies is true
        // the pnpm-sync will generate the pnpm-sync.json based on lockfile
        if (this.rushConfiguration.isPnpm && (experiments === null || experiments === void 0 ? void 0 : experiments.usePnpmSyncForInjectedDependencies)) {
            const pnpmLockfilePath = subspace.getTempShrinkwrapFilename();
            const dotPnpmFolder = `${subspace.getSubspaceTempFolderPath()}/node_modules/.pnpm`;
            const modulesFilePath = `${subspace.getSubspaceTempFolderPath()}/node_modules/.modules.yaml`;
            // we have an edge case here
            // if a package.json has no dependencies, pnpm will still generate the pnpm-lock.yaml but not .pnpm folder
            // so we need to make sure pnpm-lock.yaml and .pnpm exists before calling the pnpmSync APIs
            if ((await FileSystem.existsAsync(pnpmLockfilePath)) &&
                (await FileSystem.existsAsync(dotPnpmFolder)) &&
                (await FileSystem.existsAsync(modulesFilePath))) {
                await pnpmSyncPrepareAsync({
                    lockfilePath: pnpmLockfilePath,
                    dotPnpmFolder,
                    lockfileId: subspace.subspaceName,
                    ensureFolderAsync: FileSystem.ensureFolderAsync.bind(FileSystem),
                    // eslint-disable-next-line @typescript-eslint/naming-convention
                    readPnpmLockfile: async (lockfilePath, options) => {
                        const pnpmLockFolder = path.dirname(lockfilePath);
                        // TODO: Rework this to pre-parse out the version first, then load
                        // the relevant `@rushstack/rush-pnpm-kit-*` package.
                        const { lockfileFs: lockfileFsV9 } = await import('@rushstack/rush-pnpm-kit-v9');
                        const lockfileV9 = (await lockfileFsV9.readWantedLockfile(pnpmLockFolder, options
                        // TODO: pnpm-sync-lib.d.ts was at some point generalized to support multiple lockfile formats,
                        // however its API still returns a single "ILockfile" that is incompatible with the newer interfaces
                        ));
                        if (lockfileV9 === null || lockfileV9 === void 0 ? void 0 : lockfileV9.lockfileVersion.toString().startsWith('9')) {
                            return lockfileV9;
                        }
                        const { lockfileFs: lockfileFsV6 } = await import('@rushstack/rush-pnpm-kit-v8');
                        const lockfileV6 = await lockfileFsV6.readWantedLockfile(pnpmLockFolder, options);
                        if (lockfileV6 === null || lockfileV6 === void 0 ? void 0 : lockfileV6.lockfileVersion.toString().startsWith('6')) {
                            return lockfileV6;
                        }
                        return undefined;
                    },
                    logMessageCallback: (logMessageOptions) => PnpmSyncUtilities.processLogMessage(logMessageOptions, this._terminal)
                });
            }
            // clean up the out of date .pnpm-sync.json
            for (const rushProject of subspace.getProjects()) {
                const pnpmSyncJsonPath = `${rushProject.projectFolder}/${RushConstants.nodeModulesFolderName}/${RushConstants.pnpmSyncFilename}`;
                if (!existsSync(pnpmSyncJsonPath)) {
                    continue;
                }
                let existingPnpmSyncJsonFile;
                try {
                    existingPnpmSyncJsonFile = JSON.parse((await readFile(pnpmSyncJsonPath)).toString());
                    if ((existingPnpmSyncJsonFile === null || existingPnpmSyncJsonFile === void 0 ? void 0 : existingPnpmSyncJsonFile.version) !== pnpmSyncGetJsonVersion()) {
                        await unlink(pnpmSyncJsonPath);
                    }
                }
                catch (e) {
                    await unlink(pnpmSyncJsonPath);
                }
            }
        }
        // Perform any post-install work the install manager requires
        await this.postInstallAsync(subspace);
        if (!resolutionOnly) {
            // Create the marker file to indicate a successful install
            await commonTempInstallFlag.createAsync();
        }
        // Give plugins an opportunity to act after a successful install
        if (this.options.afterInstallAsync !== undefined) {
            await this.options.afterInstallAsync(subspace);
        }
        // eslint-disable-next-line no-console
        console.log('');
    }
    async canSkipInstallAsync(lastModifiedDate, subspace, variant) {
        // Based on timestamps, can we skip this install entirely?
        const potentiallyChangedFiles = [];
        // Consider the timestamp on the node_modules folder; if someone tampered with it
        // or deleted it entirely, then we can't skip this install
        potentiallyChangedFiles.push(path.join(subspace.getSubspaceTempFolderPath(), RushConstants.nodeModulesFolderName));
        // Additionally, if they pulled an updated shrinkwrap file from Git,
        // then we can't skip this install
        potentiallyChangedFiles.push(subspace.getCommittedShrinkwrapFilePath(variant));
        // Add common-versions.json file to the potentially changed files list.
        potentiallyChangedFiles.push(subspace.getCommonVersionsFilePath(variant));
        // Add pnpm-config.json file to the potentially changed files list.
        potentiallyChangedFiles.push(subspace.getPnpmConfigFilePath());
        if (this.rushConfiguration.isPnpm) {
            // If the repo is using pnpmfile.js, consider that also
            const pnpmFileFilePath = subspace.getPnpmfilePath(variant);
            const pnpmFileExists = await FileSystem.existsAsync(pnpmFileFilePath);
            if (pnpmFileExists) {
                potentiallyChangedFiles.push(pnpmFileFilePath);
            }
        }
        return await Utilities.isFileTimestampCurrentAsync(lastModifiedDate, potentiallyChangedFiles);
    }
    async prepareAsync(subspace, variant, projectImpactGraphGenerator) {
        const terminal = this._terminal;
        const { allowShrinkwrapUpdates } = this.options;
        // Check the policies
        await PolicyValidator.validatePolicyAsync(this.rushConfiguration, subspace, variant, this.options);
        await this._installGitHooksAsync();
        const approvedPackagesChecker = new ApprovedPackagesChecker(this.rushConfiguration);
        if (approvedPackagesChecker.approvedPackagesFilesAreOutOfDate) {
            approvedPackagesChecker.rewriteConfigFiles();
            if (allowShrinkwrapUpdates) {
                terminal.writeLine(Colorize.yellow('Approved package files have been updated. These updates should be committed to source control'));
            }
            else {
                throw new Error(`Approved packages files are out-of date. Run "rush update" to update them.`);
            }
        }
        // Ensure that the package manager is installed
        await InstallHelpers.ensureLocalPackageManagerAsync(this.rushConfiguration, this.rushGlobalFolder, this.options.maxInstallAttempts);
        let shrinkwrapFile = undefined;
        // (If it's a full update, then we ignore the shrinkwrap from Git since it will be overwritten)
        if (!this.options.fullUpgrade) {
            const shrinkwrapFilePath = subspace.getCommittedShrinkwrapFilePath(variant);
            try {
                shrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile({
                    packageManager: this.rushConfiguration.packageManager,
                    shrinkwrapFilePath,
                    subspaceHasNoProjects: subspace.getProjects().length === 0
                });
            }
            catch (ex) {
                terminal.writeLine();
                terminal.writeLine(`Unable to load the ${this.rushConfiguration.shrinkwrapFilePhrase}: ${ex.message}`);
                if (!allowShrinkwrapUpdates) {
                    terminal.writeLine();
                    terminal.writeLine(Colorize.red('You need to run "rush update" to fix this problem'));
                    throw new AlreadyReportedError();
                }
                shrinkwrapFile = undefined;
            }
        }
        // Write a file indicating which variant is being installed.
        // This will be used by bulk scripts to determine the correct Shrinkwrap file to track.
        const currentVariantJsonFilePath = this.rushConfiguration.currentVariantJsonFilePath;
        const currentVariantJson = {
            variant: variant !== null && variant !== void 0 ? variant : null
        };
        // Determine if the variant is already current by updating current-variant.json.
        // If nothing is written, the variant has not changed.
        const variantIsUpToDate = !(await JsonFile.saveAsync(currentVariantJson, currentVariantJsonFilePath, {
            onlyIfChanged: true
        }));
        this.rushConfiguration._currentVariantJsonLoadingPromise = undefined;
        if (this.options.variant) {
            terminal.writeLine();
            terminal.writeLine(Colorize.bold(`Using variant '${this.options.variant}' for installation.`));
        }
        else if (!variantIsUpToDate && !variant && this.rushConfiguration.variants.size > 0) {
            terminal.writeLine();
            terminal.writeLine(Colorize.bold('Using the default variant for installation.'));
        }
        const extraNpmrcLines = [];
        if (this.rushConfiguration.subspacesFeatureEnabled) {
            // Look for a monorepo level .npmrc file
            const commonNpmrcPath = `${this.rushConfiguration.commonRushConfigFolder}/.npmrc`;
            let commonNpmrcFileLines;
            try {
                commonNpmrcFileLines = (await FileSystem.readFileAsync(commonNpmrcPath)).split('\n');
            }
            catch (e) {
                if (!FileSystem.isNotExistError(e)) {
                    throw e;
                }
            }
            if (commonNpmrcFileLines) {
                extraNpmrcLines.push(...commonNpmrcFileLines);
            }
            extraNpmrcLines.push(`global-pnpmfile=${subspace.getSubspaceTempFolderPath()}/${RushConstants.pnpmfileGlobalFilename}`);
        }
        // Also copy down the committed .npmrc file, if there is one
        // "common\config\rush\.npmrc" --> "common\temp\.npmrc"
        // Also ensure that we remove any old one that may be hanging around
        const npmrcText = Utilities.syncNpmrc({
            sourceNpmrcFolder: subspace.getSubspaceConfigFolderPath(),
            targetNpmrcFolder: subspace.getSubspaceTempFolderPath(),
            linesToPrepend: extraNpmrcLines,
            createIfMissing: this.rushConfiguration.subspacesFeatureEnabled,
            supportEnvVarFallbackSyntax: this.rushConfiguration.isPnpm
        });
        this._syncNpmrcAlreadyCalled = true;
        const npmrcHash = npmrcText
            ? crypto.createHash('sha1').update(npmrcText).digest('hex')
            : undefined;
        if (this.rushConfiguration.isPnpm) {
            // Copy the committed patches folder if using pnpm
            const commonTempPnpmPatchesFolder = `${subspace.getSubspaceTempFolderPath()}/${RushConstants.pnpmPatchesFolderName}`;
            const rushPnpmPatchesFolder = subspace.getSubspacePnpmPatchesFolderPath();
            let rushPnpmPatches;
            try {
                rushPnpmPatches = await FileSystem.readFolderItemsAsync(rushPnpmPatchesFolder);
            }
            catch (e) {
                if (!FileSystem.isNotExistError(e)) {
                    throw e;
                }
            }
            if (rushPnpmPatches) {
                await FileSystem.ensureFolderAsync(commonTempPnpmPatchesFolder);
                const existingPatches = await FileSystem.readFolderItemsAsync(commonTempPnpmPatchesFolder);
                const copiedPatchNames = new Set();
                await Async.forEachAsync(rushPnpmPatches, async (patch) => {
                    const name = patch.name;
                    const sourcePath = `${rushPnpmPatchesFolder}/${name}`;
                    if (patch.isFile()) {
                        await FileSystem.copyFileAsync({
                            sourcePath,
                            destinationPath: `${commonTempPnpmPatchesFolder}/${name}`
                        });
                        copiedPatchNames.add(name);
                    }
                    else {
                        throw new Error(`Unexpected non-file item found in ${rushPnpmPatchesFolder}: ${sourcePath}`);
                    }
                }, { concurrency: 50 });
                await Async.forEachAsync(existingPatches, async (patch) => {
                    const name = patch.name;
                    if (!copiedPatchNames.has(name)) {
                        await FileSystem.deleteFileAsync(`${commonTempPnpmPatchesFolder}/${name}`);
                    }
                }, { concurrency: 50 });
            }
            else {
                await FileSystem.deleteFolderAsync(commonTempPnpmPatchesFolder);
            }
        }
        // Shim support for pnpmfile in.
        // Additionally when in workspaces, the shim implements support for common versions.
        if (this.rushConfiguration.isPnpm) {
            await PnpmfileConfiguration.writeCommonTempPnpmfileShimAsync(this.rushConfiguration, subspace.getSubspaceTempFolderPath(), subspace, variant);
            if (this.rushConfiguration.subspacesFeatureEnabled) {
                await SubspacePnpmfileConfiguration.writeCommonTempSubspaceGlobalPnpmfileAsync(this.rushConfiguration, subspace, variant);
            }
        }
        // eslint-disable-next-line prefer-const
        let [{ shrinkwrapIsUpToDate, shrinkwrapWarnings }, projectImpactGraphIsUpToDate = true] = await Promise.all([
            // Allow for package managers to do their own preparation and check that the shrinkwrap is up to date
            this.prepareCommonTempAsync(subspace, shrinkwrapFile),
            projectImpactGraphGenerator === null || projectImpactGraphGenerator === void 0 ? void 0 : projectImpactGraphGenerator.validateAsync()
        ]);
        shrinkwrapIsUpToDate = shrinkwrapIsUpToDate && !this.options.recheckShrinkwrap;
        this._syncTempShrinkwrap(subspace, variant, shrinkwrapFile);
        // Write out the reported warnings
        if (shrinkwrapWarnings.length > 0) {
            terminal.writeLine();
            terminal.writeLine(Colorize.yellow(PrintUtilities.wrapWords(`The ${this.rushConfiguration.shrinkwrapFilePhrase} contains the following issues:`)));
            for (const shrinkwrapWarning of shrinkwrapWarnings) {
                terminal.writeLine(Colorize.yellow('  ' + shrinkwrapWarning));
            }
            terminal.writeLine();
        }
        let hasErrors = false;
        // Force update if the shrinkwrap is out of date
        if (!shrinkwrapIsUpToDate && !allowShrinkwrapUpdates) {
            terminal.writeErrorLine();
            terminal.writeErrorLine(`The ${this.rushConfiguration.shrinkwrapFilePhrase} is out of date. You need to run "rush update".`);
            hasErrors = true;
        }
        if (!projectImpactGraphIsUpToDate && !allowShrinkwrapUpdates) {
            hasErrors = true;
            terminal.writeErrorLine();
            terminal.writeErrorLine(Colorize.red(`The ${RushConstants.projectImpactGraphFilename} file is missing or out of date. You need to run "rush update".`));
        }
        if (hasErrors) {
            throw new AlreadyReportedError();
        }
        return { shrinkwrapIsUpToDate, npmrcHash, projectImpactGraphIsUpToDate, variantIsUpToDate };
    }
    /**
     * Git hooks are only installed if the repo opts in by including files in /common/git-hooks
     */
    async _installGitHooksAsync() {
        const hookSource = path.join(this.rushConfiguration.commonFolder, 'git-hooks');
        const git = new Git(this.rushConfiguration);
        const hookDestination = git.getHooksFolder();
        if (FileSystem.exists(hookSource) && hookDestination) {
            const allHookFilenames = FileSystem.readFolderItemNames(hookSource);
            // Ignore the ".sample" file(s) in this folder.
            const hookFilenames = allHookFilenames.filter((x) => !/\.sample$/.test(x));
            if (hookFilenames.length > 0) {
                // eslint-disable-next-line no-console
                console.log('\n' + Colorize.bold('Found files in the "common/git-hooks" folder.'));
                if (!(await git.getIsHooksPathDefaultAsync())) {
                    const hooksPath = await git.getConfigHooksPathAsync();
                    const color = this.options.bypassPolicy ? Colorize.yellow : Colorize.red;
                    // eslint-disable-next-line no-console
                    console.error(color([
                        ' ',
                        `Rush cannot install the "common/git-hooks" scripts because your Git configuration `,
                        `specifies "core.hooksPath=${hooksPath}". You can remove the setting by running:`,
                        ' ',
                        '    git config --unset core.hooksPath',
                        ' '
                    ].join('\n')));
                    if (this.options.bypassPolicy) {
                        // If "--bypass-policy" is specified, skip installation of hooks because Rush doesn't
                        // own the hooks folder
                        return;
                    }
                    // eslint-disable-next-line no-console
                    console.error(color([
                        '(Or, to temporarily ignore this problem, invoke Rush with the ' +
                            `"${RushConstants.bypassPolicyFlagLongName}" option.)`,
                        ' '
                    ].join('\n')));
                    throw new AlreadyReportedError();
                }
                // Clear the currently installed git hooks and install fresh copies
                FileSystem.ensureEmptyFolder(hookDestination);
                // Find the relative path from Git hooks directory to the directory storing the actual scripts.
                const hookRelativePath = Path.convertToSlashes(path.relative(hookDestination, hookSource));
                // Only copy files that look like Git hook names
                const filteredHookFilenames = hookFilenames.filter((x) => /^[a-z\-]+/.test(x));
                for (const filename of filteredHookFilenames) {
                    const hookFilePath = `${hookSource}/${filename}`;
                    // Make sure the actual script in the hookSource directory has correct Linux compatible line endings
                    const originalHookFileContent = FileSystem.readFile(hookFilePath);
                    FileSystem.writeFile(hookFilePath, originalHookFileContent, {
                        convertLineEndings: NewlineKind.Lf
                    });
                    // Make sure the actual script in the hookSource directory has required permission bits
                    const originalPosixModeBits = FileSystem.getPosixModeBits(hookFilePath);
                    FileSystem.changePosixModeBits(hookFilePath, 
                    // eslint-disable-next-line no-bitwise
                    originalPosixModeBits | PosixModeBits.UserRead | PosixModeBits.UserExecute);
                    const gitLfsHookHandling = gitLfsHooks.has(filename)
                        ? `
# Inspired by https://github.com/git-lfs/git-lfs/issues/2865#issuecomment-365742940
if command -v git-lfs &> /dev/null; then
  git lfs ${filename} "$@"
fi
`
                        : '';
                    const hookFileContent = `#!/usr/bin/env bash
set -e
SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
SCRIPT_IMPLEMENTATION_PATH="$SCRIPT_DIR/${hookRelativePath}/${filename}"

if [[ -f "$SCRIPT_IMPLEMENTATION_PATH" ]]; then
  "$SCRIPT_IMPLEMENTATION_PATH" $@
else
  echo "The ${filename} Git hook no longer exists in your version of the repo. Run 'rush install' or 'rush update' to refresh your installed Git hooks." >&2
fi
${gitLfsHookHandling}
`;
                    // Create the hook file.  Important: For Bash scripts, the EOL must not be CRLF.
                    FileSystem.writeFile(path.join(hookDestination, filename), hookFileContent, {
                        convertLineEndings: NewlineKind.Lf
                    });
                    FileSystem.changePosixModeBits(path.join(hookDestination, filename), 
                    // eslint-disable-next-line no-bitwise
                    PosixModeBits.UserRead | PosixModeBits.UserExecute);
                }
                // eslint-disable-next-line no-console
                console.log('Successfully installed these Git hook scripts: ' + filteredHookFilenames.join(', ') + '\n');
            }
        }
    }
    /**
     * Used when invoking the NPM tool.  Appends the common configuration options
     * to the command-line.
     */
    pushConfigurationArgs(args, options, subspace) {
        const { offline, collectLogFile, pnpmFilterArgumentValues, onlyShrinkwrap, networkConcurrency, allowShrinkwrapUpdates, resolutionOnly } = options;
        if (offline && this.rushConfiguration.packageManager !== 'pnpm') {
            throw new Error('The "--offline" parameter is only supported when using the PNPM package manager.');
        }
        if (resolutionOnly && this.rushConfiguration.packageManager !== 'pnpm') {
            throw new Error('The "--resolution-only" parameter is only supported when using the PNPM package manager.');
        }
        if (this.rushConfiguration.packageManager === 'npm') {
            if (semver.lt(this.rushConfiguration.packageManagerToolVersion, '5.0.0')) {
                // NOTE:
                //
                // When using an npm version older than v5.0.0, we do NOT install optional dependencies for
                // Rush, because npm does not generate the shrinkwrap file consistently across platforms.
                //
                // Consider the "fsevents" package. This is a Mac specific package
                // which is an optional second-order dependency. Optional dependencies work by attempting to install
                // the package, but removes the package if the install failed.
                // This means that someone running generate on a Mac WILL have fsevents included in their shrinkwrap.
                // When someone using Windows attempts to install from the shrinkwrap, the install will fail.
                //
                // If someone generates the shrinkwrap using Windows, then fsevents will NOT be listed in the shrinkwrap.
                // When someone using Mac attempts to install from the shrinkwrap, they will NOT have the
                // optional dependency installed.
                //
                // This issue has been fixed as of npm v5.0.0: https://github.com/npm/npm/releases/tag/v5.0.0
                //
                // For more context, see https://github.com/microsoft/rushstack/issues/761#issuecomment-428689600
                args.push('--no-optional');
            }
            args.push('--cache', this.rushConfiguration.npmCacheFolder);
            args.push('--tmp', this.rushConfiguration.npmTmpFolder);
            if (collectLogFile) {
                args.push('--verbose');
            }
        }
        else if (this.rushConfiguration.isPnpm) {
            // Only explicitly define the store path if `pnpmStore` is using the default, or has been set to
            // 'local'.  If `pnpmStore` = 'global', then allow PNPM to use the system's default
            // path.  In all cases, this will be overridden by RUSH_PNPM_STORE_PATH
            if (this.rushConfiguration.pnpmOptions.pnpmStore === 'local' ||
                EnvironmentConfiguration.pnpmStorePathOverride) {
                args.push('--store', this.rushConfiguration.pnpmOptions.pnpmStorePath);
                if (semver.gte(this.rushConfiguration.packageManagerToolVersion, '6.10.0')) {
                    args.push(`${pnpmCacheDirParameter}=${this.rushConfiguration.pnpmOptions.pnpmStorePath}`);
                    args.push(`${pnpmStateDirParameter}=${this.rushConfiguration.pnpmOptions.pnpmStorePath}`);
                }
            }
            const { pnpmVerifyStoreIntegrity } = EnvironmentConfiguration;
            if (pnpmVerifyStoreIntegrity !== undefined) {
                args.push(`--verify-store-integrity`, `${pnpmVerifyStoreIntegrity}`);
            }
            const { configuration: experiments } = this.rushConfiguration.experimentsConfiguration;
            if (experiments.usePnpmFrozenLockfileForRushInstall && !allowShrinkwrapUpdates) {
                args.push('--frozen-lockfile');
                if (pnpmFilterArgumentValues.length > 0 &&
                    Number.parseInt(this.rushConfiguration.packageManagerToolVersion, 10) >= 8 // PNPM Major version 8+
                ) {
                    // On pnpm@8, disable the "dedupe-peer-dependents" feature when doing a filtered CI install so that filters take effect.
                    args.push('--config.dedupe-peer-dependents=false');
                }
            }
            else if (experiments.usePnpmPreferFrozenLockfileForRushUpdate) {
                // In workspaces, we want to avoid unnecessary lockfile churn
                args.push('--prefer-frozen-lockfile');
            }
            else {
                // Ensure that Rush's tarball dependencies get synchronized properly with the pnpm-lock.yaml file.
                // See this GitHub issue: https://github.com/pnpm/pnpm/issues/1342
                args.push('--no-prefer-frozen-lockfile');
            }
            if (onlyShrinkwrap) {
                args.push(`--lockfile-only`);
            }
            if (collectLogFile) {
                args.push('--reporter', 'ndjson');
            }
            if (networkConcurrency) {
                args.push('--network-concurrency', networkConcurrency.toString());
            }
            if (offline) {
                args.push('--offline');
            }
            if (this.rushConfiguration.pnpmOptions.strictPeerDependencies === false) {
                args.push('--no-strict-peer-dependencies');
            }
            else {
                args.push('--strict-peer-dependencies');
            }
            if (resolutionOnly) {
                args.push('--resolution-only');
            }
            /*
              If user set auto-install-peers in pnpm-config.json only, use the value in pnpm-config.json
              If user set auto-install-peers in pnpm-config.json and .npmrc, use the value in pnpm-config.json
              If user set auto-install-peers in .npmrc only, do nothing, let pnpm handle it
              If user does not set auto-install-peers in both pnpm-config.json and .npmrc, rush will default it to "false"
            */
            const isAutoInstallPeersInNpmrc = isVariableSetInNpmrcFile(subspace.getSubspaceConfigFolderPath(), 'auto-install-peers', this.rushConfiguration.isPnpm);
            let autoInstallPeers = this.rushConfiguration.pnpmOptions.autoInstallPeers;
            if (autoInstallPeers !== undefined) {
                if (isAutoInstallPeersInNpmrc) {
                    this._terminal.writeWarningLine(`Warning: PNPM's auto-install-peers is specified in both .npmrc and pnpm-config.json. ` +
                        `The value in pnpm-config.json will take precedence.`);
                }
            }
            else if (!isAutoInstallPeersInNpmrc) {
                // if auto-install-peers isn't specified in either .npmrc or pnpm-config.json,
                // then rush will default it to "false"
                autoInstallPeers = false;
            }
            if (autoInstallPeers !== undefined) {
                args.push(`--config.auto-install-peers=${autoInstallPeers}`);
            }
            /*
              If user set resolution-mode in pnpm-config.json only, use the value in pnpm-config.json
              If user set resolution-mode in pnpm-config.json and .npmrc, use the value in pnpm-config.json
              If user set resolution-mode in .npmrc only, do nothing, let pnpm handle it
              If user does not set resolution-mode in pnpm-config.json and .npmrc, rush will default it to "highest"
            */
            const isResolutionModeInNpmrc = isVariableSetInNpmrcFile(subspace.getSubspaceConfigFolderPath(), 'resolution-mode', this.rushConfiguration.isPnpm);
            let resolutionMode = this.rushConfiguration.pnpmOptions.resolutionMode;
            if (resolutionMode) {
                if (isResolutionModeInNpmrc) {
                    this._terminal.writeWarningLine(`Warning: PNPM's resolution-mode is specified in both .npmrc and pnpm-config.json. ` +
                        `The value in pnpm-config.json will take precedence.`);
                }
            }
            else if (!isResolutionModeInNpmrc) {
                // if resolution-mode isn't specified in either .npmrc or pnpm-config.json,
                // then rush will default it to "highest"
                resolutionMode = 'highest';
            }
            if (resolutionMode) {
                args.push(`--config.resolutionMode=${resolutionMode}`);
            }
            if (semver.satisfies(this.rushConfiguration.packageManagerToolVersion, '6.32.12 - 6.33.x || 7.0.1 - 7.8.x')) {
                this._terminal.writeWarningLine(`Warning: Your ${RushConstants.rushJsonFilename} specifies a pnpmVersion with a known issue ` +
                    'that may cause unintended version selections.' +
                    " It's recommended to upgrade to PNPM >=6.34.0 or >=7.9.0. " +
                    'For details see: https://rushjs.io/link/pnpm-issue-5132');
            }
            if (semver.gte(this.rushConfiguration.packageManagerToolVersion, '7.9.0') ||
                semver.satisfies(this.rushConfiguration.packageManagerToolVersion, '^6.34.0')) {
                args.push(pnpmIgnoreCompatibilityDbParameter);
            }
        }
        else if (this.rushConfiguration.packageManager === 'yarn') {
            args.push('--link-folder', 'yarn-link');
            args.push('--cache-folder', this.rushConfiguration.yarnCacheFolder);
            // Without this option, Yarn will sometimes stop and ask for user input on STDIN
            // (e.g. "Which command would you like to run?").
            args.push('--non-interactive');
            if (networkConcurrency) {
                args.push('--network-concurrency', networkConcurrency.toString());
            }
            if (this.rushConfiguration.yarnOptions.ignoreEngines) {
                args.push('--ignore-engines');
            }
            if (collectLogFile) {
                args.push('--verbose');
            }
        }
    }
    async _checkIfReleaseIsPublishedAsync() {
        const lastCheckFile = path.join(this.rushGlobalFolder.nodeSpecificPath, 'rush-' + Rush.version, 'last-check.flag');
        if (FileSystem.exists(lastCheckFile)) {
            let cachedResult = undefined;
            try {
                // NOTE: mtimeMs is not supported yet in Node.js 6.x
                const nowMs = new Date().getTime();
                const ageMs = nowMs - FileSystem.getStatistics(lastCheckFile).mtime.getTime();
                const HOUR = 60 * 60 * 1000;
                // Is the cache too old?
                if (ageMs < 24 * HOUR) {
                    // No, read the cached result
                    cachedResult = JsonFile.load(lastCheckFile);
                }
            }
            catch (e) {
                // Unable to parse file
            }
            if (cachedResult === 'error') {
                throw new Error('Unable to contact server');
            }
            if (cachedResult === true || cachedResult === false) {
                return cachedResult;
            }
        }
        // Before we start the network operation, record a failed state.  If the process exits for some reason,
        // this will record the error.  It will also update the timestamp to prevent other Rush instances
        // from attempting to update the file.
        await JsonFile.saveAsync('error', lastCheckFile, { ensureFolderExists: true });
        try {
            // For this check we use the official registry, not the private registry
            const publishedRelease = await this._queryIfReleaseIsPublishedAsync('https://registry.npmjs.org:443');
            // Cache the result
            await JsonFile.saveAsync(publishedRelease, lastCheckFile, { ensureFolderExists: true });
            return publishedRelease;
        }
        catch (error) {
            await JsonFile.saveAsync('error', lastCheckFile, { ensureFolderExists: true });
            throw error;
        }
    }
    // Helper for checkIfReleaseIsPublished()
    async _queryIfReleaseIsPublishedAsync(registryUrl) {
        let queryUrl = registryUrl;
        if (queryUrl[-1] !== '/') {
            queryUrl += '/';
        }
        // Note that the "@" symbol does not normally get URL-encoded
        queryUrl += RushConstants.rushPackageName.replace('/', '%2F');
        const { WebClient } = await import('../../utilities/WebClient');
        const webClient = new WebClient();
        webClient.userAgent = `pnpm/? npm/? node/${process.version} ${os.platform()} ${os.arch()}`;
        webClient.accept = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*';
        const response = await webClient.fetchAsync(queryUrl);
        if (!response.ok) {
            throw new Error('Failed to query');
        }
        const data = await response.getJsonAsync();
        let url;
        try {
            if (!data.versions[Rush.version]) {
                // Version was not published
                return false;
            }
            url = data.versions[Rush.version].dist.tarball;
            if (!url) {
                throw new Error(`URL not found`);
            }
        }
        catch (e) {
            throw new Error('Error parsing response');
        }
        // Make sure the tarball wasn't deleted from the CDN
        webClient.accept = '*/*';
        const response2 = await webClient.fetchAsync(url);
        if (!response2.ok) {
            if (response2.status === 404) {
                return false;
            }
            else {
                throw new Error('Failed to fetch');
            }
        }
        return true;
    }
    _syncTempShrinkwrap(subspace, variant, shrinkwrapFile) {
        const committedShrinkwrapFileName = subspace.getCommittedShrinkwrapFilePath(variant);
        if (shrinkwrapFile) {
            Utilities.syncFile(committedShrinkwrapFileName, subspace.getTempShrinkwrapFilename());
            Utilities.syncFile(committedShrinkwrapFileName, subspace.getTempShrinkwrapPreinstallFilename());
        }
        else {
            // Otherwise delete the temporary file
            FileSystem.deleteFile(subspace.getTempShrinkwrapFilename());
            if (this.rushConfiguration.isPnpm) {
                // Workaround for https://github.com/pnpm/pnpm/issues/1890
                //
                // When "rush update --full" is run, Rush deletes "common/temp/pnpm-lock.yaml"
                // so that a new lockfile will be generated. However "pnpm install" by design will try to recover
                // "pnpm-lock.yaml" from "common/temp/node_modules/.pnpm/lock.yaml", which may prevent a full upgrade.
                // Deleting both files ensures that a new lockfile will always be generated.
                const pnpmPackageManager = this.rushConfiguration
                    .packageManagerWrapper;
                FileSystem.deleteFile(path.join(subspace.getSubspaceTempFolderPath(), pnpmPackageManager.internalShrinkwrapRelativePath));
            }
        }
    }
    async validateNpmSetupAsync() {
        if (this._npmSetupValidated) {
            return;
        }
        if (!this.options.bypassPolicy) {
            const setupPackageRegistry = new SetupPackageRegistry({
                rushConfiguration: this.rushConfiguration,
                isDebug: this.options.debug,
                syncNpmrcAlreadyCalled: this._syncNpmrcAlreadyCalled
            });
            const valid = await setupPackageRegistry.checkOnlyAsync();
            if (!valid) {
                // eslint-disable-next-line no-console
                console.error();
                // eslint-disable-next-line no-console
                console.error(Colorize.red('ERROR: NPM credentials are missing or expired'));
                // eslint-disable-next-line no-console
                console.error();
                // eslint-disable-next-line no-console
                console.error(Colorize.bold('==> Please run "rush setup" to update your NPM token. ' +
                    `(Or append "${RushConstants.bypassPolicyFlagLongName}" to proceed anyway.)`));
                throw new AlreadyReportedError();
            }
        }
        this._npmSetupValidated = true;
    }
}
//# sourceMappingURL=BaseInstallManager.js.map