"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CliRepl = void 0;
const errors_1 = require("@mongosh/errors");
const mongodb_redact_1 = require("mongodb-redact");
const i18n_1 = __importDefault(require("@mongosh/i18n"));
const bson_1 = require("bson");
const service_provider_node_driver_1 = require("@mongosh/service-provider-node-driver");
const snippet_manager_1 = require("@mongosh/snippet-manager");
const editor_1 = require("@mongosh/editor");
const askpassword_1 = __importDefault(require("askpassword"));
const events_1 = require("events");
const js_yaml_1 = __importDefault(require("js-yaml"));
const mongodb_connection_string_url_1 = __importDefault(require("mongodb-connection-string-url"));
const semver_1 = __importDefault(require("semver"));
const build_info_1 = require("./build-info");
const config_directory_1 = require("./config-directory");
const error_codes_1 = require("./error-codes");
const format_json_1 = require("./format-json");
const mongodb_log_writer_1 = require("mongodb-log-writer");
const mongosh_repl_1 = __importDefault(require("./mongosh-repl"));
const logging_1 = require("@mongosh/logging");
const logging_2 = require("@mongosh/logging");
const types_1 = require("@mongosh/types");
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const get_os_info_1 = require("./get-os-info");
const update_notification_manager_1 = require("./update-notification-manager");
const startup_timing_1 = require("./startup-timing");
const devtools_proxy_support_1 = require("@mongodb-js/devtools-proxy-support");
const format_output_1 = require("./format-output");
const device_id_1 = require("./device-id");
const CONNECTING = 'cli-repl.cli-repl.connecting';
class CliRepl {
    constructor(options) {
        this.globalConfig = null;
        this.toggleableAnalytics = new logging_2.ToggleableAnalytics();
        this.warnedAboutInaccessibleFiles = false;
        this.isContainerizedEnvironment = false;
        this.hasOnDiskTelemetryId = false;
        this.proxyOptions = {
            useEnvironmentVariableProxies: true,
        };
        this.fetchMongoshUpdateUrlRegardlessOfCiEnvironment = false;
        this.exitingForStartupError = false;
        this.deviceId = undefined;
        this.osInfo = undefined;
        this.closeAbortController = new AbortController();
        this.getGlibcVersion = build_info_1.getGlibcVersion;
        this.bus = new events_1.EventEmitter();
        this.cliOptions = options.shellCliOptions;
        this.input = options.input;
        this.output = options.output;
        this.promptOutput = options.promptOutput ?? options.output;
        this.analyticsOptions = options.analyticsOptions;
        this.onExit = options.onExit;
        const id = new bson_1.ObjectId().toHexString();
        this.config = {
            userId: id,
            telemetryAnonymousId: id,
            enableTelemetry: true,
        };
        this.getCryptLibraryPaths = options.getCryptLibraryPaths;
        this.globalConfigPaths = options.globalConfigPaths ?? [];
        this.shellHomeDirectory = new config_directory_1.ShellHomeDirectory(options.shellHomePaths);
        this.configDirectory = new config_directory_1.ConfigManager(this.shellHomeDirectory)
            .on('error', (err) => {
            this.bus.emit('mongosh:error', err, 'config');
        })
            .on('new-config', (config) => {
            this.hasOnDiskTelemetryId = !!(config.userId || config.telemetryAnonymousId);
            this.setTelemetryEnabled(config.enableTelemetry);
            this.bus.emit('mongosh:new-user', {
                userId: config.userId,
                anonymousId: config.telemetryAnonymousId,
            });
        })
            .on('update-config', (config) => {
            this.hasOnDiskTelemetryId = !!(config.userId || config.telemetryAnonymousId);
            this.setTelemetryEnabled(config.enableTelemetry);
            this.bus.emit('mongosh:update-user', {
                userId: config.userId,
                anonymousId: config.telemetryAnonymousId,
            });
        });
        this.agent = (0, devtools_proxy_support_1.useOrCreateAgent)(this.proxyOptions);
        const baseFetch = (0, devtools_proxy_support_1.createFetch)(this.agent ?? {});
        const { version } = require('../package.json');
        this.fetch =
            ({ includeDeviceId }) => async (url, init) => {
                const [{ os_type, os_release, os_arch, os_linux_dist, os_linux_release }, deviceId,] = await Promise.all([
                    this.getOsInfo(),
                    (async () => {
                        if (!includeDeviceId ||
                            this.forceDisableTelemetry ||
                            !(await this.getConfig('enableTelemetry'))) {
                            return 'disabled';
                        }
                        return await this.getDeviceId();
                    })(),
                ]);
                const userAgentTags = [
                    `mongosh/${version}`,
                    os_type,
                    os_release,
                    os_arch,
                    os_linux_dist ?? '',
                    os_linux_release ?? '',
                    deviceId,
                ]
                    .map((s) => s.replace(/[^a-zA-Z0-9./_-]/g, '_'))
                    .join('; ');
                return baseFetch(url, {
                    ...init,
                    headers: {
                        ...init?.headers,
                        'User-Agent': userAgentTags,
                    },
                });
            };
        this.updateNotificationManager = new update_notification_manager_1.UpdateNotificationManager({
            fetch: this.fetch({ includeDeviceId: true }),
        });
        this.output.on('error', (err) => {
            this.bus.emit('mongosh:error', err, 'io');
        });
        let jsContext = this.cliOptions.jsContext;
        const { willEnterInteractiveMode, quiet } = CliRepl.getFileAndEvalInfo(this.cliOptions);
        if (jsContext === 'auto' || !jsContext) {
            jsContext = willEnterInteractiveMode ? 'repl' : 'plain-vm';
        }
        const deepInspect = this.cliOptions.deepInspect ?? willEnterInteractiveMode;
        this.mongoshRepl = new mongosh_repl_1.default({
            ...options,
            shellCliOptions: { ...this.cliOptions, jsContext, quiet, deepInspect },
            nodeReplOptions: options.nodeReplOptions ?? {
                terminal: process.env.MONGOSH_FORCE_TERMINAL ? true : undefined,
            },
            bus: this.bus,
            ioProvider: this,
        });
        this.setupOIDCTokenDumpListener();
    }
    getLogPath() {
        return this.logWriter?.logFilePath ?? undefined;
    }
    async getIsContainerizedEnvironment() {
        try {
            await fs_1.promises.stat('/.dockerenv');
            return true;
        }
        catch {
            try {
                const cgroup = await fs_1.promises.readFile('/proc/self/cgroup', 'utf8');
                return /\b(docker|lxc|kubepods)\b/.test(cgroup);
            }
            catch {
                return false;
            }
        }
    }
    get forceDisableTelemetry() {
        return (this.globalConfig?.forceDisableTelemetry ||
            (this.isContainerizedEnvironment && !this.mongoshRepl.isInteractive) ||
            !!process.env.MONGOSH_FORCE_DISABLE_TELEMETRY_FOR_TESTING);
    }
    async startLogging() {
        if (!this.loggingAndTelemetry) {
            throw new Error('Logging and telemetry not setup');
        }
        const customLogLocation = await this.getConfig('logLocation');
        this.logManager ??= new mongodb_log_writer_1.MongoLogManager({
            directory: customLogLocation || this.shellHomeDirectory.localPath('.'),
            prefix: customLogLocation ? 'mongosh_' : undefined,
            retentionDays: await this.getConfig('logRetentionDays'),
            gzip: await this.getConfig('logCompressionEnabled'),
            maxLogFileCount: await this.getConfig('logMaxFileCount'),
            retentionGB: await this.getConfig('logRetentionGB'),
            onerror: (err) => this.bus.emit('mongosh:error', err, 'log'),
            onwarn: (err, path) => this.warnAboutInaccessibleFile(err, path),
        });
        void this.logManager
            .cleanupOldLogFiles()
            .catch((err) => {
            this.bus.emit('mongosh:error', err, 'log');
        })
            .finally(() => {
            (0, startup_timing_1.markTime)(types_1.TimingCategories.Logging, 'cleaned up log files');
        });
        if (!this.logWriter) {
            this.logWriter ??= await this.logManager.createLogWriter();
            const { quiet } = CliRepl.getFileAndEvalInfo(this.cliOptions);
            if (!quiet) {
                this.output.write(`Current Mongosh Log ID:\t${this.logWriter.logId}\n`);
            }
            (0, startup_timing_1.markTime)(types_1.TimingCategories.Logging, 'instantiated log writer');
        }
        this.loggingAndTelemetry.attachLogger(this.logWriter);
        this.logWriter.info('MONGOSH', (0, mongodb_log_writer_1.mongoLogId)(1_000_000_000), 'log', 'Starting log', {
            execPath: process.execPath,
            envInfo: (0, mongodb_redact_1.redact)(this.getLoggedEnvironmentVariables()),
            ...(await (0, build_info_1.buildInfo)()),
        });
        (0, startup_timing_1.markTime)(types_1.TimingCategories.Logging, 'logged initial message');
    }
    async start(driverUri, driverOptions) {
        try {
            return await this._start(driverUri, driverOptions);
        }
        catch (err) {
            this.exitingForStartupError = true;
            await this.close();
            throw err;
        }
    }
    getDeviceId() {
        return (this.deviceId ??= (async () => (this.deviceId = await (0, device_id_1.getDeviceIdForMongosh)({
            bus: this.bus,
            signal: this.closeAbortController.signal,
        })))());
    }
    getOsInfo() {
        return (this.osInfo ??= (async () => (this.osInfo = await (0, get_os_info_1.getOsInfo)()))());
    }
    async _start(driverUri, driverOptions) {
        const { version } = require('../package.json');
        await this.verifyNodeVersion();
        (0, startup_timing_1.markTime)(types_1.TimingCategories.REPLInstantiation, 'verified node version');
        void this.getDeviceId();
        void this.getOsInfo();
        this.isContainerizedEnvironment =
            await this.getIsContainerizedEnvironment();
        (0, startup_timing_1.markTime)(types_1.TimingCategories.REPLInstantiation, 'checked containerized environment');
        if (!this.cliOptions.nodb) {
            const cs = new mongodb_connection_string_url_1.default(driverUri);
            const searchParams = cs.typedSearchParams();
            if (!searchParams.get('appName')) {
                searchParams.set('appName', `mongosh ${version}`);
            }
            if (this.isPasswordMissingURI(cs)) {
                cs.password = encodeURIComponent(await this.requirePassword());
            }
            if (await this.isTlsKeyFilePasswordMissingURI(searchParams)) {
                const keyFilePassword = encodeURIComponent(await this.requirePassword('Enter TLS key file password'));
                searchParams.set('tlsCertificateKeyFilePassword', keyFilePassword);
            }
            this.ensurePasswordFieldIsPresentInAuth(driverOptions);
            driverUri = cs.toString();
        }
        try {
            await this.shellHomeDirectory.ensureExists();
        }
        catch (err) {
            this.warnAboutInaccessibleFile(err);
        }
        (0, startup_timing_1.markTime)(types_1.TimingCategories.REPLInstantiation, 'ensured shell homedir');
        let analyticsSetupError = null;
        try {
            await this.setupAnalytics();
        }
        catch (err) {
            analyticsSetupError = err;
        }
        (0, startup_timing_1.markTime)(types_1.TimingCategories.Telemetry, 'created analytics instance');
        this.loggingAndTelemetry = (0, logging_1.setupLoggingAndTelemetry)({
            bus: this.bus,
            analytics: this.toggleableAnalytics,
            userTraits: {
                platform: process.platform,
                arch: process.arch,
                is_containerized: this.isContainerizedEnvironment,
                ...(await this.getOsInfo()),
            },
            mongoshVersion: version,
            deviceId: this.getDeviceId(),
        });
        (0, startup_timing_1.markTime)(types_1.TimingCategories.Telemetry, 'completed telemetry setup');
        if (analyticsSetupError) {
            this.bus.emit('mongosh:error', analyticsSetupError, 'analytics');
        }
        try {
            this.config = await this.configDirectory.generateOrReadConfig(this.config);
        }
        catch (err) {
            this.warnAboutInaccessibleFile(err);
        }
        this.globalConfig = await this.loadGlobalConfigFile();
        (0, startup_timing_1.markTime)(types_1.TimingCategories.UserConfigLoading, 'read global config files');
        await this.setLoggingEnabled(!(await this.getConfig('disableLogging')));
        void this.fetchMongoshUpdateUrl();
        if (driverOptions.autoEncryption) {
            const origExtraOptions = driverOptions.autoEncryption.extraOptions ?? {};
            if (origExtraOptions.cryptSharedLibPath) {
                this.cachedCryptLibraryPath = Promise.resolve({
                    cryptSharedLibPath: origExtraOptions.cryptSharedLibPath,
                });
            }
            const extraOptions = {
                ...origExtraOptions,
                ...(await this.getCryptLibraryOptions()),
            };
            driverOptions.autoEncryption = {
                ...driverOptions.autoEncryption,
                extraOptions,
            };
        }
        if (Object.keys(driverOptions.autoEncryption ?? {}).join(',') ===
            'extraOptions') {
            delete driverOptions.autoEncryption;
        }
        driverOptions = await this.prepareOIDCOptions(driverUri, driverOptions);
        (0, startup_timing_1.markTime)(types_1.TimingCategories.DriverSetup, 'prepared OIDC options');
        let initialServiceProvider;
        try {
            initialServiceProvider = await this.connect(driverUri, driverOptions);
        }
        catch (err) {
            if (typeof err === 'object' &&
                err?.constructor.name === 'MongoDBOIDCError' &&
                !String(driverOptions.oidc?.allowedFlows)?.includes('device-auth')) {
                err.message +=
                    '\nConsider specifying --oidcFlows=auth-code,device-auth if you are running mongosh in an environment without browser access.';
            }
            throw err;
        }
        (0, startup_timing_1.markTime)(types_1.TimingCategories.DriverSetup, 'completed SP setup');
        const [moreRecentMongoshVersion, currentVersionCTA, { installationMethod },] = await Promise.all([
            this.getMoreRecentMongoshVersion(),
            this.updateNotificationManager.getGreetingCTAForCurrentVersion(),
            (0, build_info_1.buildInfo)(),
        ]);
        const initialized = await this.mongoshRepl.initialize(initialServiceProvider, {
            moreRecentMongoshVersion,
            currentVersionCTA,
            isHomebrew: installationMethod === 'homebrew',
        });
        (0, startup_timing_1.markTime)(types_1.TimingCategories.REPLInstantiation, 'initialized mongosh repl');
        this.injectReplFunctions();
        const { commandLineLoadFiles, evalScripts, willEnterInteractiveMode, willExecuteCommandLineScripts, } = CliRepl.getFileAndEvalInfo(this.cliOptions);
        if ((evalScripts.length === 0 ||
            this.cliOptions.shell ||
            commandLineLoadFiles.length > 0) &&
            this.cliOptions.json) {
            throw new errors_1.MongoshRuntimeError('Cannot use --json without --eval or with --shell or with extra files');
        }
        let snippetManager;
        if (this.config.snippetIndexSourceURLs !== '') {
            snippetManager = snippet_manager_1.SnippetManager.create({
                installdir: this.shellHomeDirectory.roamingPath('snippets'),
                instanceState: this.mongoshRepl.runtimeState().instanceState,
                skipInitialIndexLoad: !willEnterInteractiveMode,
                fetch: this.fetch({ includeDeviceId: false }),
            });
        }
        editor_1.Editor.create({
            input: this.input,
            vscodeDir: this.shellHomeDirectory.rcPath('.vscode'),
            tmpDir: this.shellHomeDirectory.localPath('editor'),
            instanceState: this.mongoshRepl.runtimeState().instanceState,
            loadExternalCode: this.mongoshRepl.loadExternalCode.bind(this.mongoshRepl),
        });
        (0, startup_timing_1.markTime)(types_1.TimingCategories.REPLInstantiation, 'set up editor');
        if (willExecuteCommandLineScripts) {
            this.mongoshRepl.setIsInteractive(willEnterInteractiveMode);
            this.bus.emit('mongosh:start-session', {
                isInteractive: false,
                jsContext: this.mongoshRepl.jsContext(),
                timings: (0, startup_timing_1.summariseTimingData)((0, startup_timing_1.getTimingData)()),
            });
            this.bus.emit('mongosh:start-loading-cli-scripts', {
                usesShellOption: !!this.cliOptions.shell,
            });
            const exitCode = await this.loadCommandLineFilesAndEval(commandLineLoadFiles, evalScripts);
            if (exitCode !== 0) {
                await this.exit(exitCode);
                return;
            }
            if (!this.cliOptions.shell) {
                this.setTelemetryEnabled(await this.getConfig('enableTelemetry'));
                await this.exit(0);
                return;
            }
        }
        else {
            this.mongoshRepl.setIsInteractive(true);
        }
        if (!this.cliOptions.norc) {
            (0, startup_timing_1.markTime)(types_1.TimingCategories.SnippetLoading, 'start load snippets');
            await snippetManager?.loadAllSnippets();
            (0, startup_timing_1.markTime)(types_1.TimingCategories.SnippetLoading, 'done load snippets');
        }
        (0, startup_timing_1.markTime)(types_1.TimingCategories.ResourceFileLoading, 'loading rc files');
        await this.loadRcFiles();
        (0, startup_timing_1.markTime)(types_1.TimingCategories.ResourceFileLoading, 'loaded rc files');
        this.verifyPlatformSupport();
        this.setTelemetryEnabled(await this.getConfig('enableTelemetry'));
        this.bus.emit('mongosh:start-mongosh-repl', { version });
        (0, startup_timing_1.markTime)(types_1.TimingCategories.REPLInstantiation, 'starting repl');
        await this.mongoshRepl.startRepl(initialized);
        this.bus.emit('mongosh:start-session', {
            isInteractive: true,
            jsContext: this.mongoshRepl.jsContext(),
            timings: (0, startup_timing_1.summariseTimingData)((0, startup_timing_1.getTimingData)()),
        });
    }
    static getFileAndEvalInfo(cliOptions) {
        const commandLineLoadFiles = cliOptions.fileNames ?? [];
        const evalScripts = cliOptions.eval ?? [];
        const willExecuteCommandLineScripts = commandLineLoadFiles.length > 0 || evalScripts.length > 0;
        const willEnterInteractiveMode = !willExecuteCommandLineScripts || !!cliOptions.shell;
        const quiet = cliOptions.quiet ?? !(cliOptions.verbose ?? willEnterInteractiveMode);
        return {
            commandLineLoadFiles,
            evalScripts,
            willEnterInteractiveMode,
            willExecuteCommandLineScripts,
            quiet,
        };
    }
    injectReplFunctions() {
        const functions = {
            async buildInfo() {
                return await (0, build_info_1.buildInfo)();
            },
        };
        const { context } = this.mongoshRepl.runtimeState();
        for (const [name, impl] of Object.entries(functions)) {
            context[name] = (...args) => {
                return Object.assign(impl(...args), {
                    [Symbol.for('@@mongosh.syntheticPromise')]: true,
                });
            };
        }
    }
    async setupAnalytics() {
        if (process.env.IS_MONGOSH_EVERGREEN_CI &&
            !this.analyticsOptions?.alwaysEnable) {
            throw new Error('no analytics setup for the mongosh CI environment');
        }
        const apiKey = this.analyticsOptions?.apiKey ??
            (await (0, build_info_1.buildInfo)({ withSegmentApiKey: true })).segmentApiKey;
        if (!apiKey) {
            throw new Error('no analytics API key defined');
        }
        const { Analytics } = require('@segment/analytics-node');
        this.segmentAnalytics = new Analytics({
            writeKey: apiKey,
            maxRetries: 0,
            httpRequestTimeout: 1000,
            ...this.analyticsOptions,
        });
        this.toggleableAnalytics = new logging_2.ToggleableAnalytics(new logging_2.SampledAnalytics({
            target: new logging_2.ThrottledAnalytics({
                target: this.segmentAnalytics,
                throttle: {
                    rate: 30,
                    metadataPath: this.shellHomeDirectory.paths.shellLocalDataPath,
                },
            }),
            sampling: () => !!process.env.MONGOSH_ANALYTICS_SAMPLE || Math.random() <= 0.01,
        }));
    }
    async setLoggingEnabled(enabled) {
        if (enabled) {
            await this.startLogging();
        }
        else {
            await this.loggingAndTelemetry?.detachLogger();
        }
    }
    setTelemetryEnabled(enabled) {
        if (this.globalConfig === null) {
            return;
        }
        if (enabled && this.hasOnDiskTelemetryId && !this.forceDisableTelemetry) {
            this.toggleableAnalytics.enable();
        }
        else {
            this.toggleableAnalytics.disable();
        }
    }
    async loadCommandLineFilesAndEval(files, evalScripts) {
        let lastEvalResult;
        let exitCode = 0;
        try {
            (0, startup_timing_1.markTime)(types_1.TimingCategories.Eval, 'start eval scripts');
            for (const script of evalScripts) {
                this.bus.emit('mongosh:eval-cli-script');
                lastEvalResult = await this.mongoshRepl.loadExternalCode(script, '@(shell eval)');
            }
            (0, startup_timing_1.markTime)(types_1.TimingCategories.Eval, 'finished eval scripts');
        }
        catch (err) {
            exitCode = 1;
            if (this.cliOptions.json) {
                lastEvalResult = err;
            }
            else {
                throw err;
            }
        }
        if (lastEvalResult !== undefined) {
            let formattedResult;
            if (this.cliOptions.json) {
                try {
                    formattedResult = (0, format_json_1.formatForJSONOutput)(lastEvalResult, this.cliOptions.json);
                }
                catch (e) {
                    exitCode = 1;
                    formattedResult = (0, format_json_1.formatForJSONOutput)(e, this.cliOptions.json);
                }
            }
            else {
                formattedResult = this.mongoshRepl.writer(lastEvalResult, this.cliOptions.deepInspect !== false
                    ? format_output_1.fullDepthInspectOptions
                    : undefined);
            }
            this.output.write(formattedResult + '\n');
        }
        (0, startup_timing_1.markTime)(types_1.TimingCategories.Eval, 'wrote eval output');
        (0, startup_timing_1.markTime)(types_1.TimingCategories.EvalFile, 'start loading external files');
        const { quiet } = CliRepl.getFileAndEvalInfo(this.cliOptions);
        for (const file of files) {
            if (!quiet) {
                this.output.write(`Loading file: ${this.clr(file, 'mongosh:filename')}\n`);
            }
            await this.mongoshRepl.loadExternalFile(file);
        }
        (0, startup_timing_1.markTime)(types_1.TimingCategories.EvalFile, 'finished external files');
        return exitCode;
    }
    async loadRcFiles() {
        if (this.cliOptions.norc) {
            return;
        }
        const legacyPath = this.shellHomeDirectory.rcPath('.mongorc.js');
        const mongoshrcPath = this.shellHomeDirectory.rcPath('.mongoshrc.js');
        const mongoshrcMisspelledPath = this.shellHomeDirectory.rcPath('.mongoshrc');
        let hasMongoshRc = false;
        try {
            await fs_1.promises.stat(mongoshrcPath);
            hasMongoshRc = true;
        }
        catch {
        }
        if (hasMongoshRc) {
            try {
                this.bus.emit('mongosh:mongoshrc-load');
                await this.mongoshRepl.loadExternalFile(mongoshrcPath);
            }
            catch (err) {
                this.output.write(this.clr('Error while running ~/.mongoshrc.js:\n', 'mongosh:warning'));
                this.output.write(this.mongoshRepl.writer(err) + '\n');
            }
            return;
        }
        if (this.cliOptions.quiet) {
            return;
        }
        let hasLegacyRc = false;
        try {
            await fs_1.promises.stat(legacyPath);
            hasLegacyRc = true;
        }
        catch {
        }
        if (hasLegacyRc) {
            this.bus.emit('mongosh:mongoshrc-mongorc-warn');
            const msg = 'Warning: Found ~/.mongorc.js, but not ~/.mongoshrc.js. ~/.mongorc.js will not be loaded.\n' +
                '  You may want to copy or rename ~/.mongorc.js to ~/.mongoshrc.js.\n';
            this.output.write(this.clr(msg, 'mongosh:warning'));
            return;
        }
        let hasMisspelledFilename = false;
        try {
            await fs_1.promises.stat(mongoshrcMisspelledPath);
            hasMisspelledFilename = true;
        }
        catch {
        }
        if (hasMisspelledFilename) {
            const msg = 'Warning: Found ~/.mongoshrc, but not ~/.mongoshrc.js. Did you forget to add .js?\n';
            this.output.write(this.clr(msg, 'mongosh:warning'));
        }
    }
    async loadGlobalConfigFile() {
        let fileContents = '';
        let filename = '';
        for (filename of this.globalConfigPaths) {
            try {
                fileContents = await fs_1.promises.readFile(filename, 'utf8');
                break;
            }
            catch (err) {
                if (err?.code !== 'ENOENT') {
                    this.bus.emit('mongosh:error', err, 'config');
                }
            }
        }
        this.bus.emit('mongosh:globalconfig-load', {
            filename,
            found: fileContents.length > 0,
        });
        try {
            let config;
            if (fileContents.trim().startsWith('{')) {
                config = bson_1.EJSON.parse(fileContents);
            }
            else {
                config = js_yaml_1.default.load(fileContents)?.mongosh ?? {};
            }
            for (const [key, value] of Object.entries(config)) {
                const validationResult = await types_1.CliUserConfigValidator.validate(key, value);
                if (validationResult) {
                    const msg = `Warning: Ignoring config option "${key}" from ${filename}: ${validationResult}\n`;
                    this.output.write(this.clr(msg, 'mongosh:warning'));
                    delete config[key];
                }
            }
            return config;
        }
        catch (err) {
            this.bus.emit('mongosh:error', err, 'config');
            const msg = `Warning: Could not parse global configuration file at ${filename}: ${err?.message}\n`;
            this.output.write(this.clr(msg, 'mongosh:warning'));
            return {};
        }
    }
    warnAboutInaccessibleFile(err, path) {
        this.bus.emit('mongosh:error', err, 'config');
        if (this.warnedAboutInaccessibleFiles) {
            return;
        }
        this.warnedAboutInaccessibleFiles = true;
        const msg = `Warning: Could not access file${path ? 'at ' + path : ''}: ${err.message}\n`;
        this.output.write(this.clr(msg, 'mongosh:warning'));
    }
    async connect(driverUri, driverOptions) {
        const { quiet } = CliRepl.getFileAndEvalInfo(this.cliOptions);
        if (!this.cliOptions.nodb && !quiet) {
            this.output.write(i18n_1.default.__(CONNECTING) +
                '\t\t' +
                this.clr((0, mongodb_redact_1.redactConnectionString)(driverUri), 'mongosh:uri') +
                '\n');
        }
        return await service_provider_node_driver_1.NodeDriverServiceProvider.connect(driverUri, driverOptions, this.cliOptions, this.bus);
    }
    getHistoryFilePath() {
        return this.shellHomeDirectory.roamingPath('mongosh_repl_history');
    }
    async getConfig(key) {
        return (this.config[key] ??
            this.globalConfig?.[key] ??
            new types_1.CliUserConfig()[key]);
    }
    async setConfig(key, value) {
        if (key === 'forceDisableTelemetry') {
            throw new errors_1.MongoshRuntimeError("The 'forceDisableTelemetry' setting cannot be modified");
        }
        this.config[key] = value;
        if (key === 'enableTelemetry') {
            if (this.forceDisableTelemetry) {
                throw new errors_1.MongoshRuntimeError("Cannot modify telemetry settings while 'forceDisableTelemetry' is set to true");
            }
            this.setTelemetryEnabled(this.config.enableTelemetry);
            this.bus.emit('mongosh:update-user', {
                userId: this.config.userId,
                anonymousId: this.config.telemetryAnonymousId,
            });
        }
        if (key === 'disableLogging') {
            await this.setLoggingEnabled(!value);
        }
        try {
            await this.configDirectory.writeConfigFile(this.config);
        }
        catch (err) {
            this.warnAboutInaccessibleFile(err, this.configDirectory.path());
        }
        return 'success';
    }
    listConfigOptions() {
        const hiddenKeys = [
            'userId',
            'telemetryAnonymousId',
            'disableGreetingMessage',
            'forceDisableTelemetry',
        ];
        const keys = Object.keys(new types_1.CliUserConfig());
        return keys.filter((key) => !hiddenKeys.includes(key));
    }
    async verifyNodeVersion() {
        if (process.env.MONGOSH_SKIP_NODE_VERSION_CHECK) {
            return;
        }
        const { engines } = require('../package.json');
        const baseNodeVersion = process.version.replace(/-.*$/, '');
        if (!semver_1.default.satisfies(baseNodeVersion, engines.node)) {
            const warning = new errors_1.MongoshWarning(`Mismatched node version. Required version: ${engines.node}. Currently using: ${process.version}. Exiting...\n\n`, error_codes_1.CliReplErrors.NodeVersionMismatch);
            await this._fatalError(warning);
        }
    }
    verifyPlatformSupport() {
        const { quiet } = CliRepl.getFileAndEvalInfo(this.cliOptions);
        if (quiet) {
            return;
        }
        const glibcVersion = this.getGlibcVersion();
        const warnings = [];
        const RECOMMENDED_GLIBC = '>=2.28.0';
        const RECOMMENDED_OPENSSL = '>=3.0.0';
        const RECOMMENDED_NODEJS = '>=24.0.0';
        const semverRangeCheck = (semverLikeVersion, range) => {
            const semverVersion = semver_1.default.valid(semver_1.default.coerce(semverLikeVersion));
            if (!semverVersion) {
                return true;
            }
            return semver_1.default.satisfies(semverVersion, range);
        };
        const satisfiesGLIBCRequirement = (glibcVersion) => semverRangeCheck(glibcVersion, RECOMMENDED_GLIBC);
        if (glibcVersion !== undefined &&
            !satisfiesGLIBCRequirement(glibcVersion)) {
            warnings.push('  - Using mongosh on the current operating system is deprecated, and support may be removed in a future release.');
        }
        const satisfiesOpenSSLRequirement = (opensslVersion) => semverRangeCheck(opensslVersion, RECOMMENDED_OPENSSL);
        if (!satisfiesOpenSSLRequirement(process.versions.openssl)) {
            warnings.push('  - Using mongosh with OpenSSL versions lower than 3.0.0 is deprecated, and support may be removed in a future release.');
        }
        if (!semver_1.default.satisfies(process.version, RECOMMENDED_NODEJS)) {
            warnings.push('  - Using mongosh with Node.js versions lower than 24.0.0 is deprecated, and support may be removed in a future release.');
        }
        if (warnings.length) {
            const deprecationWarning = [
                'Deprecation warnings:',
                ...warnings,
                'See https://www.mongodb.com/docs/mongodb-shell/install/#supported-operating-systems for documentation on supported platforms.',
            ].join('\n');
            this.output.write(this.clr(`\n${deprecationWarning}\n`, 'mongosh:warning'));
        }
    }
    isPasswordMissingURI(cs) {
        return !!(cs.username &&
            !cs.password &&
            ['', 'MONGODB-CR', 'PLAIN', 'SCRAM-SHA-1', 'SCRAM-SHA-256'].includes(cs.searchParams.get('authMechanism') ?? ''));
    }
    async isTlsKeyFilePasswordMissingURI(searchParams) {
        const tlsCertificateKeyFile = searchParams.get('tlsCertificateKeyFile');
        const tlsCertificateKeyFilePassword = searchParams.get('tlsCertificateKeyFilePassword');
        if (tlsCertificateKeyFile && !tlsCertificateKeyFilePassword) {
            const { contents } = await this.readFileUTF8(tlsCertificateKeyFile);
            return (contents.search(/(ENCRYPTED PRIVATE KEY|Proc-Type: 4,ENCRYPTED)/) !== -1);
        }
        return false;
    }
    ensurePasswordFieldIsPresentInAuth(driverOptions) {
        if (driverOptions.auth &&
            driverOptions.auth.username &&
            !('password' in driverOptions.auth)) {
            driverOptions.auth.password = undefined;
        }
    }
    async requirePassword(passwordPrompt = 'Enter password') {
        const passwordPromise = (0, askpassword_1.default)({
            input: this.input,
            output: this.promptOutput,
            replacementCharacter: '*',
        });
        this.promptOutput.write(`${passwordPrompt}: `);
        try {
            try {
                return (await passwordPromise).toString();
            }
            finally {
                this.promptOutput.write('\n');
            }
        }
        catch (error) {
            await this._fatalError(error);
        }
        return '';
    }
    async _fatalError(error) {
        this.bus.emit('mongosh:error', error, 'fatal');
        this.output.write(this.mongoshRepl.formatError(error) + '\n');
        return this.exit(1);
    }
    async close() {
        return (this.closingPromise ??= (async () => {
            this.closeAbortController.abort();
            (0, startup_timing_1.markTime)(types_1.TimingCategories.REPLInstantiation, 'start closing');
            await this.mongoshRepl.close();
            this.agent?.destroy();
            if (!this.output.destroyed) {
                if (this.output.writableEnded) {
                    try {
                        await (0, events_1.once)(this.output, 'close');
                    }
                    catch {
                    }
                }
                else {
                    await new Promise((resolve) => this.output.write('', resolve));
                }
            }
            (0, startup_timing_1.markTime)(types_1.TimingCategories.REPLInstantiation, 'output flushed');
            const analytics = this.toggleableAnalytics;
            let flushError = null;
            let flushDuration = null;
            await this.loggingAndTelemetry?.flush();
            if (analytics) {
                const flushStart = Date.now();
                try {
                    await analytics.flush();
                    (0, startup_timing_1.markTime)(types_1.TimingCategories.Telemetry, 'flushed analytics');
                }
                catch (err) {
                    flushError = err.message;
                }
                finally {
                    flushDuration = Date.now() - flushStart;
                }
            }
            this.logWriter?.info('MONGOSH', (0, mongodb_log_writer_1.mongoLogId)(1_000_000_045), 'analytics', 'Flushed outstanding data', {
                flushError,
                flushDuration,
            });
            await this.logWriter?.flush();
            (0, startup_timing_1.markTime)(types_1.TimingCategories.Logging, 'flushed log writer');
            this.bus.emit('mongosh:closed');
        })());
    }
    async exit(code) {
        await this.close();
        if (this.exitingForStartupError)
            return new Promise(() => undefined);
        await this.onExit(code);
        const error = new errors_1.MongoshInternalError('onExit() unexpectedly returned');
        this.bus.emit('mongosh:error', error, 'fatal');
        throw error;
    }
    async readFileUTF8(filename) {
        const resolved = path_1.default.resolve(filename);
        return {
            contents: await fs_1.promises.readFile(resolved, 'utf8'),
            absolutePath: resolved,
        };
    }
    clr(text, style) {
        return this.mongoshRepl.clr(text, style);
    }
    async getCryptLibraryOptions() {
        if (!this.getCryptLibraryPaths) {
            throw new errors_1.MongoshInternalError('This instance of mongosh is not configured for in-use encryption');
        }
        return (this.cachedCryptLibraryPath ??= this.getCryptLibraryPaths(this.bus));
    }
    bugReportErrorMessageInfo() {
        return `Please include the log file for this session (${this.logWriter?.logFilePath}).`;
    }
    getLoggedEnvironmentVariables() {
        const { EDITOR, NODE_OPTIONS, TERM } = process.env;
        return { EDITOR, NODE_OPTIONS, TERM };
    }
    async prepareOIDCOptions(driverUri, driverOptionsIn) {
        const driverOptions = {
            oidc: {},
            authMechanismProperties: {},
            ...driverOptionsIn,
        };
        driverOptions.oidc.allowedFlows ??= ['auth-code'];
        driverOptions.oidc.notifyDeviceFlow ??= ({ verificationUrl, userCode }) => {
            this.output.write('\n' +
                `Visit the following URL to complete authentication: ${this.clr(verificationUrl, 'mongosh:uri')}\n` +
                `Enter the following code on that page: ${this.clr(userCode, 'mongosh:uri')}\nWaiting...\n`);
        };
        driverOptions.proxy ??= this.proxyOptions;
        driverOptions.applyProxyToOIDC ??= true;
        const [redirectURI, trustedEndpoints, browser] = await Promise.all([
            this.getConfig('oidcRedirectURI'),
            this.getConfig('oidcTrustedEndpoints'),
            this.getConfig('browser'),
        ]);
        if (redirectURI !== undefined) {
            driverOptions.oidc.redirectURI ??= redirectURI;
        }
        if (browser !== undefined) {
            driverOptions.oidc.openBrowser ??=
                browser !== false ? { command: browser } : browser;
        }
        if (trustedEndpoints !== undefined) {
            driverOptions.authMechanismProperties.ALLOWED_HOSTS ??= trustedEndpoints;
        }
        if (process.env.MONGOSH_OIDC_PARENT_HANDLE) {
            driverOptions.parentHandle ??= process.env.MONGOSH_OIDC_PARENT_HANDLE;
        }
        return driverOptions;
    }
    async fetchMongoshUpdateUrl() {
        const { quiet } = CliRepl.getFileAndEvalInfo(this.cliOptions);
        if (quiet ||
            (!this.fetchMongoshUpdateUrlRegardlessOfCiEnvironment &&
                (process.env.CI ||
                    process.env.IS_CI ||
                    this.isContainerizedEnvironment))) {
            return;
        }
        try {
            const updateURL = (await this.getConfig('updateURL')).trim();
            if (!updateURL)
                return;
            const { version: currentVersion } = require('../package.json');
            const localFilePath = this.shellHomeDirectory.localPath('update-metadata.json');
            this.bus.emit('mongosh:fetching-update-metadata', {
                updateURL,
                localFilePath,
                currentVersion,
            });
            await this.updateNotificationManager.fetchUpdateMetadata(updateURL, localFilePath, currentVersion);
            this.bus.emit('mongosh:fetching-update-metadata-complete', {
                latest: await this.updateNotificationManager.getLatestVersionIfMoreRecent(''),
                currentVersion,
                hasGreetingCTA: !!(await this.updateNotificationManager.getGreetingCTAForCurrentVersion()),
            });
        }
        catch (err) {
            this.bus.emit('mongosh:error', err, 'startup');
        }
    }
    async getMoreRecentMongoshVersion() {
        const { version } = require('../package.json');
        return await this.updateNotificationManager.getLatestVersionIfMoreRecent(process.env
            .MONGOSH_ASSUME_DIFFERENT_VERSION_FOR_UPDATE_NOTIFICATION_TEST ||
            version);
    }
    setupOIDCTokenDumpListener() {
        function tryParseJWT(token, redact) {
            if (!token)
                return token;
            const jwtParts = token.split('.');
            if (jwtParts.length === 3 &&
                jwtParts.every((part) => Buffer.from(part, 'base64url')
                    .toString('base64url')
                    .replace(/=+$/, '') === part.replace(/=+$/, ''))) {
                const [header, payload] = jwtParts.map((part) => {
                    try {
                        return JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
                    }
                    catch {
                    }
                });
                if (redact === 'include-secrets') {
                    return { header, payload, signature: jwtParts[2] };
                }
                if (header && payload) {
                    return { header, payload };
                }
            }
            return redact === 'include-secrets' ? token : '<non-JWT token>';
        }
        let lastServerIdPInfo;
        const { oidcDumpTokens } = this.cliOptions;
        if (oidcDumpTokens) {
            this.bus.on('mongodb-oidc-plugin:received-server-params', ({ params: { idpInfo } }) => {
                lastServerIdPInfo = idpInfo;
            });
            this.bus.on('mongodb-oidc-plugin:auth-succeeded', ({ tokenType, refreshToken, expiresAt, passIdTokenAsAccessToken, tokens: { accessToken: at, refreshToken: rt, idToken: idt }, }) => {
                const printable = {
                    lastServerIdPInfo: lastServerIdPInfo && {
                        issuer: lastServerIdPInfo?.issuer,
                        clientId: lastServerIdPInfo?.clientId,
                        requestScopes: lastServerIdPInfo?.requestScopes,
                    },
                    tokenType,
                    refreshToken,
                    expiresAt,
                    passIdTokenAsAccessToken,
                    tokens: oidcDumpTokens === 'include-secrets'
                        ? {
                            accessToken: tryParseJWT(at, 'include-secrets'),
                            refreshToken: tryParseJWT(rt, 'include-secrets'),
                            idToken: tryParseJWT(idt, 'include-secrets'),
                        }
                        : {
                            accessToken: tryParseJWT(at, 'redact'),
                            idToken: tryParseJWT(idt, 'redact'),
                        },
                };
                this.output.write('\n' +
                    this.clr('----- BEGIN OIDC TOKEN DUMP -----', 'mongosh:section-header') +
                    '\n' +
                    JSON.stringify(printable, null, 2) +
                    '\n' +
                    this.clr('----- END OIDC TOKEN DUMP -----', 'mongosh:section-header') +
                    '\n');
            });
        }
    }
}
exports.CliRepl = CliRepl;
//# sourceMappingURL=cli-repl.js.map