/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir, platform } from 'node:os';
import * as dotenv from 'dotenv';
import process from 'node:process';
import { FatalConfigError, GEMINI_CONFIG_DIR as GEMINI_DIR, getErrorMessage, Storage, } from '@google/gemini-cli-core';
import stripJsonComments from 'strip-json-comments';
import { DefaultLight } from '../ui/themes/default-light.js';
import { DefaultDark } from '../ui/themes/default.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { getSettingsSchema, } from './settingsSchema.js';
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
import { customDeepMerge } from '../utils/deepMerge.js';
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
import { disableExtension } from './extension.js';
function getMergeStrategyForPath(path) {
    let current = undefined;
    let currentSchema = getSettingsSchema();
    for (const key of path) {
        if (!currentSchema || !currentSchema[key]) {
            return undefined;
        }
        current = currentSchema[key];
        currentSchema = current.properties;
    }
    return current?.mergeStrategy;
}
export const SETTINGS_DIRECTORY_NAME = '.gemini';
export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
const MIGRATE_V2_OVERWRITE = true;
const MIGRATION_MAP = {
    accessibility: 'ui.accessibility',
    allowedTools: 'tools.allowed',
    allowMCPServers: 'mcp.allowed',
    autoAccept: 'tools.autoAccept',
    autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
    bugCommand: 'advanced.bugCommand',
    chatCompression: 'model.chatCompression',
    checkpointing: 'general.checkpointing',
    coreTools: 'tools.core',
    contextFileName: 'context.fileName',
    customThemes: 'ui.customThemes',
    customWittyPhrases: 'ui.customWittyPhrases',
    debugKeystrokeLogging: 'general.debugKeystrokeLogging',
    disableAutoUpdate: 'general.disableAutoUpdate',
    disableUpdateNag: 'general.disableUpdateNag',
    dnsResolutionOrder: 'advanced.dnsResolutionOrder',
    enableMessageBusIntegration: 'tools.enableMessageBusIntegration',
    enablePromptCompletion: 'general.enablePromptCompletion',
    enforcedAuthType: 'security.auth.enforcedType',
    excludeTools: 'tools.exclude',
    excludeMCPServers: 'mcp.excluded',
    excludedProjectEnvVars: 'advanced.excludedEnvVars',
    extensionManagement: 'experimental.extensionManagement',
    extensions: 'extensions',
    fileFiltering: 'context.fileFiltering',
    folderTrustFeature: 'security.folderTrust.featureEnabled',
    folderTrust: 'security.folderTrust.enabled',
    hasSeenIdeIntegrationNudge: 'ide.hasSeenNudge',
    hideWindowTitle: 'ui.hideWindowTitle',
    hideTips: 'ui.hideTips',
    hideBanner: 'ui.hideBanner',
    hideFooter: 'ui.hideFooter',
    hideCWD: 'ui.footer.hideCWD',
    hideSandboxStatus: 'ui.footer.hideSandboxStatus',
    hideModelInfo: 'ui.footer.hideModelInfo',
    hideContextSummary: 'ui.hideContextSummary',
    showMemoryUsage: 'ui.showMemoryUsage',
    showLineNumbers: 'ui.showLineNumbers',
    showCitations: 'ui.showCitations',
    ideMode: 'ide.enabled',
    includeDirectories: 'context.includeDirectories',
    loadMemoryFromIncludeDirectories: 'context.loadFromIncludeDirectories',
    maxSessionTurns: 'model.maxSessionTurns',
    mcpServers: 'mcpServers',
    mcpServerCommand: 'mcp.serverCommand',
    memoryImportFormat: 'context.importFormat',
    memoryDiscoveryMaxDirs: 'context.discoveryMaxDirs',
    model: 'model.name',
    preferredEditor: 'general.preferredEditor',
    sandbox: 'tools.sandbox',
    selectedAuthType: 'security.auth.selectedType',
    shouldUseNodePtyShell: 'tools.shell.enableInteractiveShell',
    shellPager: 'tools.shell.pager',
    shellShowColor: 'tools.shell.showColor',
    skipNextSpeakerCheck: 'model.skipNextSpeakerCheck',
    summarizeToolOutput: 'model.summarizeToolOutput',
    telemetry: 'telemetry',
    theme: 'ui.theme',
    toolDiscoveryCommand: 'tools.discoveryCommand',
    toolCallCommand: 'tools.callCommand',
    usageStatisticsEnabled: 'privacy.usageStatisticsEnabled',
    useExternalAuth: 'security.auth.useExternal',
    useRipgrep: 'tools.useRipgrep',
    vimMode: 'general.vimMode',
};
export function getSystemSettingsPath() {
    if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) {
        return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
    }
    if (platform() === 'darwin') {
        return '/Library/Application Support/GeminiCli/settings.json';
    }
    else if (platform() === 'win32') {
        return 'C:\\ProgramData\\gemini-cli\\settings.json';
    }
    else {
        return '/etc/gemini-cli/settings.json';
    }
}
export function getSystemDefaultsPath() {
    if (process.env['GEMINI_CLI_SYSTEM_DEFAULTS_PATH']) {
        return process.env['GEMINI_CLI_SYSTEM_DEFAULTS_PATH'];
    }
    return path.join(path.dirname(getSystemSettingsPath()), 'system-defaults.json');
}
export var SettingScope;
(function (SettingScope) {
    SettingScope["User"] = "User";
    SettingScope["Workspace"] = "Workspace";
    SettingScope["System"] = "System";
    SettingScope["SystemDefaults"] = "SystemDefaults";
})(SettingScope || (SettingScope = {}));
function setNestedProperty(obj, path, value) {
    const keys = path.split('.');
    const lastKey = keys.pop();
    if (!lastKey)
        return;
    let current = obj;
    for (const key of keys) {
        if (current[key] === undefined) {
            current[key] = {};
        }
        const next = current[key];
        if (typeof next === 'object' && next !== null) {
            current = next;
        }
        else {
            // This path is invalid, so we stop.
            return;
        }
    }
    current[lastKey] = value;
}
export function needsMigration(settings) {
    // A file needs migration if it contains any top-level key that is moved to a
    // nested location in V2.
    const hasV1Keys = Object.entries(MIGRATION_MAP).some(([v1Key, v2Path]) => {
        if (v1Key === v2Path || !(v1Key in settings)) {
            return false;
        }
        // If a key exists that is both a V1 key and a V2 container (like 'model'),
        // we need to check the type. If it's an object, it's a V2 container and not
        // a V1 key that needs migration.
        if (KNOWN_V2_CONTAINERS.has(v1Key) &&
            typeof settings[v1Key] === 'object' &&
            settings[v1Key] !== null) {
            return false;
        }
        return true;
    });
    return hasV1Keys;
}
function migrateSettingsToV2(flatSettings) {
    if (!needsMigration(flatSettings)) {
        return null;
    }
    const v2Settings = {};
    const flatKeys = new Set(Object.keys(flatSettings));
    for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) {
        if (flatKeys.has(oldKey)) {
            setNestedProperty(v2Settings, newPath, flatSettings[oldKey]);
            flatKeys.delete(oldKey);
        }
    }
    // Preserve mcpServers at the top level
    if (flatSettings['mcpServers']) {
        v2Settings['mcpServers'] = flatSettings['mcpServers'];
        flatKeys.delete('mcpServers');
    }
    // Carry over any unrecognized keys
    for (const remainingKey of flatKeys) {
        const existingValue = v2Settings[remainingKey];
        const newValue = flatSettings[remainingKey];
        if (typeof existingValue === 'object' &&
            existingValue !== null &&
            !Array.isArray(existingValue) &&
            typeof newValue === 'object' &&
            newValue !== null &&
            !Array.isArray(newValue)) {
            const pathAwareGetStrategy = (path) => getMergeStrategyForPath([remainingKey, ...path]);
            v2Settings[remainingKey] = customDeepMerge(pathAwareGetStrategy, {}, newValue, existingValue);
        }
        else {
            v2Settings[remainingKey] = newValue;
        }
    }
    return v2Settings;
}
function getNestedProperty(obj, path) {
    const keys = path.split('.');
    let current = obj;
    for (const key of keys) {
        if (typeof current !== 'object' || current === null || !(key in current)) {
            return undefined;
        }
        current = current[key];
    }
    return current;
}
const REVERSE_MIGRATION_MAP = Object.fromEntries(Object.entries(MIGRATION_MAP).map(([key, value]) => [value, key]));
// Dynamically determine the top-level keys from the V2 settings structure.
const KNOWN_V2_CONTAINERS = new Set(Object.values(MIGRATION_MAP).map((path) => path.split('.')[0]));
export function migrateSettingsToV1(v2Settings) {
    const v1Settings = {};
    const v2Keys = new Set(Object.keys(v2Settings));
    for (const [newPath, oldKey] of Object.entries(REVERSE_MIGRATION_MAP)) {
        const value = getNestedProperty(v2Settings, newPath);
        if (value !== undefined) {
            v1Settings[oldKey] = value;
            v2Keys.delete(newPath.split('.')[0]);
        }
    }
    // Preserve mcpServers at the top level
    if (v2Settings['mcpServers']) {
        v1Settings['mcpServers'] = v2Settings['mcpServers'];
        v2Keys.delete('mcpServers');
    }
    // Carry over any unrecognized keys
    for (const remainingKey of v2Keys) {
        const value = v2Settings[remainingKey];
        if (value === undefined) {
            continue;
        }
        // Don't carry over empty objects that were just containers for migrated settings.
        if (KNOWN_V2_CONTAINERS.has(remainingKey) &&
            typeof value === 'object' &&
            value !== null &&
            !Array.isArray(value) &&
            Object.keys(value).length === 0) {
            continue;
        }
        v1Settings[remainingKey] = value;
    }
    return v1Settings;
}
function mergeSettings(system, systemDefaults, user, workspace, isTrusted) {
    const safeWorkspace = isTrusted ? workspace : {};
    // Settings are merged with the following precedence (last one wins for
    // single values):
    // 1. System Defaults
    // 2. User Settings
    // 3. Workspace Settings
    // 4. System Settings (as overrides)
    return customDeepMerge(getMergeStrategyForPath, {}, // Start with an empty object
    systemDefaults, user, safeWorkspace, system);
}
export class LoadedSettings {
    constructor(system, systemDefaults, user, workspace, isTrusted, migratedInMemorScopes) {
        this.system = system;
        this.systemDefaults = systemDefaults;
        this.user = user;
        this.workspace = workspace;
        this.isTrusted = isTrusted;
        this.migratedInMemorScopes = migratedInMemorScopes;
        this._merged = this.computeMergedSettings();
    }
    system;
    systemDefaults;
    user;
    workspace;
    isTrusted;
    migratedInMemorScopes;
    _merged;
    get merged() {
        return this._merged;
    }
    computeMergedSettings() {
        return mergeSettings(this.system.settings, this.systemDefaults.settings, this.user.settings, this.workspace.settings, this.isTrusted);
    }
    forScope(scope) {
        switch (scope) {
            case SettingScope.User:
                return this.user;
            case SettingScope.Workspace:
                return this.workspace;
            case SettingScope.System:
                return this.system;
            case SettingScope.SystemDefaults:
                return this.systemDefaults;
            default:
                throw new Error(`Invalid scope: ${scope}`);
        }
    }
    setValue(scope, key, value) {
        const settingsFile = this.forScope(scope);
        setNestedProperty(settingsFile.settings, key, value);
        setNestedProperty(settingsFile.originalSettings, key, value);
        this._merged = this.computeMergedSettings();
        saveSettings(settingsFile);
    }
}
function findEnvFile(startDir) {
    let currentDir = path.resolve(startDir);
    while (true) {
        // prefer gemini-specific .env under GEMINI_DIR
        const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
        if (fs.existsSync(geminiEnvPath)) {
            return geminiEnvPath;
        }
        const envPath = path.join(currentDir, '.env');
        if (fs.existsSync(envPath)) {
            return envPath;
        }
        const parentDir = path.dirname(currentDir);
        if (parentDir === currentDir || !parentDir) {
            // check .env under home as fallback, again preferring gemini-specific .env
            const homeGeminiEnvPath = path.join(homedir(), GEMINI_DIR, '.env');
            if (fs.existsSync(homeGeminiEnvPath)) {
                return homeGeminiEnvPath;
            }
            const homeEnvPath = path.join(homedir(), '.env');
            if (fs.existsSync(homeEnvPath)) {
                return homeEnvPath;
            }
            return null;
        }
        currentDir = parentDir;
    }
}
export function setUpCloudShellEnvironment(envFilePath) {
    // Special handling for GOOGLE_CLOUD_PROJECT in Cloud Shell:
    // Because GOOGLE_CLOUD_PROJECT in Cloud Shell tracks the project
    // set by the user using "gcloud config set project" we do not want to
    // use its value. So, unless the user overrides GOOGLE_CLOUD_PROJECT in
    // one of the .env files, we set the Cloud Shell-specific default here.
    if (envFilePath && fs.existsSync(envFilePath)) {
        const envFileContent = fs.readFileSync(envFilePath);
        const parsedEnv = dotenv.parse(envFileContent);
        if (parsedEnv['GOOGLE_CLOUD_PROJECT']) {
            // .env file takes precedence in Cloud Shell
            process.env['GOOGLE_CLOUD_PROJECT'] = parsedEnv['GOOGLE_CLOUD_PROJECT'];
        }
        else {
            // If not in .env, set to default and override global
            process.env['GOOGLE_CLOUD_PROJECT'] = 'cloudshell-gca';
        }
    }
    else {
        // If no .env file, set to default and override global
        process.env['GOOGLE_CLOUD_PROJECT'] = 'cloudshell-gca';
    }
}
export function loadEnvironment(settings) {
    const envFilePath = findEnvFile(process.cwd());
    if (!isWorkspaceTrusted(settings).isTrusted) {
        return;
    }
    // Cloud Shell environment variable handling
    if (process.env['CLOUD_SHELL'] === 'true') {
        setUpCloudShellEnvironment(envFilePath);
    }
    if (envFilePath) {
        // Manually parse and load environment variables to handle exclusions correctly.
        // This avoids modifying environment variables that were already set from the shell.
        try {
            const envFileContent = fs.readFileSync(envFilePath, 'utf-8');
            const parsedEnv = dotenv.parse(envFileContent);
            const excludedVars = settings?.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
            const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR);
            for (const key in parsedEnv) {
                if (Object.hasOwn(parsedEnv, key)) {
                    // If it's a project .env file, skip loading excluded variables.
                    if (isProjectEnvFile && excludedVars.includes(key)) {
                        continue;
                    }
                    // Load variable only if it's not already set in the environment.
                    if (!Object.hasOwn(process.env, key)) {
                        process.env[key] = parsedEnv[key];
                    }
                }
            }
        }
        catch (_e) {
            // Errors are ignored to match the behavior of `dotenv.config({ quiet: true })`.
        }
    }
}
/**
 * Loads settings from user and workspace directories.
 * Project settings override user settings.
 */
export function loadSettings(workspaceDir = process.cwd()) {
    let systemSettings = {};
    let systemDefaultSettings = {};
    let userSettings = {};
    let workspaceSettings = {};
    const settingsErrors = [];
    const systemSettingsPath = getSystemSettingsPath();
    const systemDefaultsPath = getSystemDefaultsPath();
    const migratedInMemorScopes = new Set();
    // Resolve paths to their canonical representation to handle symlinks
    const resolvedWorkspaceDir = path.resolve(workspaceDir);
    const resolvedHomeDir = path.resolve(homedir());
    let realWorkspaceDir = resolvedWorkspaceDir;
    try {
        // fs.realpathSync gets the "true" path, resolving any symlinks
        realWorkspaceDir = fs.realpathSync(resolvedWorkspaceDir);
    }
    catch (_e) {
        // This is okay. The path might not exist yet, and that's a valid state.
    }
    // We expect homedir to always exist and be resolvable.
    const realHomeDir = fs.realpathSync(resolvedHomeDir);
    const workspaceSettingsPath = new Storage(workspaceDir).getWorkspaceSettingsPath();
    const loadAndMigrate = (filePath, scope) => {
        try {
            if (fs.existsSync(filePath)) {
                const content = fs.readFileSync(filePath, 'utf-8');
                const rawSettings = JSON.parse(stripJsonComments(content));
                if (typeof rawSettings !== 'object' ||
                    rawSettings === null ||
                    Array.isArray(rawSettings)) {
                    settingsErrors.push({
                        message: 'Settings file is not a valid JSON object.',
                        path: filePath,
                    });
                    return { settings: {} };
                }
                let settingsObject = rawSettings;
                if (needsMigration(settingsObject)) {
                    const migratedSettings = migrateSettingsToV2(settingsObject);
                    if (migratedSettings) {
                        if (MIGRATE_V2_OVERWRITE) {
                            try {
                                fs.renameSync(filePath, `${filePath}.orig`);
                                fs.writeFileSync(filePath, JSON.stringify(migratedSettings, null, 2), 'utf-8');
                            }
                            catch (e) {
                                console.error(`Error migrating settings file on disk: ${getErrorMessage(e)}`);
                            }
                        }
                        else {
                            migratedInMemorScopes.add(scope);
                        }
                        settingsObject = migratedSettings;
                    }
                }
                return { settings: settingsObject, rawJson: content };
            }
        }
        catch (error) {
            settingsErrors.push({
                message: getErrorMessage(error),
                path: filePath,
            });
        }
        return { settings: {} };
    };
    const systemResult = loadAndMigrate(systemSettingsPath, SettingScope.System);
    const systemDefaultsResult = loadAndMigrate(systemDefaultsPath, SettingScope.SystemDefaults);
    const userResult = loadAndMigrate(USER_SETTINGS_PATH, SettingScope.User);
    let workspaceResult = {
        settings: {},
        rawJson: undefined,
    };
    if (realWorkspaceDir !== realHomeDir) {
        workspaceResult = loadAndMigrate(workspaceSettingsPath, SettingScope.Workspace);
    }
    const systemOriginalSettings = structuredClone(systemResult.settings);
    const systemDefaultsOriginalSettings = structuredClone(systemDefaultsResult.settings);
    const userOriginalSettings = structuredClone(userResult.settings);
    const workspaceOriginalSettings = structuredClone(workspaceResult.settings);
    // Environment variables for runtime use
    systemSettings = resolveEnvVarsInObject(systemResult.settings);
    systemDefaultSettings = resolveEnvVarsInObject(systemDefaultsResult.settings);
    userSettings = resolveEnvVarsInObject(userResult.settings);
    workspaceSettings = resolveEnvVarsInObject(workspaceResult.settings);
    // Support legacy theme names
    if (userSettings.ui?.theme === 'VS') {
        userSettings.ui.theme = DefaultLight.name;
    }
    else if (userSettings.ui?.theme === 'VS2015') {
        userSettings.ui.theme = DefaultDark.name;
    }
    if (workspaceSettings.ui?.theme === 'VS') {
        workspaceSettings.ui.theme = DefaultLight.name;
    }
    else if (workspaceSettings.ui?.theme === 'VS2015') {
        workspaceSettings.ui.theme = DefaultDark.name;
    }
    // For the initial trust check, we can only use user and system settings.
    const initialTrustCheckSettings = customDeepMerge(getMergeStrategyForPath, {}, systemSettings, userSettings);
    const isTrusted = isWorkspaceTrusted(initialTrustCheckSettings).isTrusted ?? true;
    // Create a temporary merged settings object to pass to loadEnvironment.
    const tempMergedSettings = mergeSettings(systemSettings, systemDefaultSettings, userSettings, workspaceSettings, isTrusted);
    // loadEnviroment depends on settings so we have to create a temp version of
    // the settings to avoid a cycle
    loadEnvironment(tempMergedSettings);
    // Create LoadedSettings first
    if (settingsErrors.length > 0) {
        const errorMessages = settingsErrors.map((error) => `Error in ${error.path}: ${error.message}`);
        throw new FatalConfigError(`${errorMessages.join('\n')}\nPlease fix the configuration file(s) and try again.`);
    }
    return new LoadedSettings({
        path: systemSettingsPath,
        settings: systemSettings,
        originalSettings: systemOriginalSettings,
        rawJson: systemResult.rawJson,
    }, {
        path: systemDefaultsPath,
        settings: systemDefaultSettings,
        originalSettings: systemDefaultsOriginalSettings,
        rawJson: systemDefaultsResult.rawJson,
    }, {
        path: USER_SETTINGS_PATH,
        settings: userSettings,
        originalSettings: userOriginalSettings,
        rawJson: userResult.rawJson,
    }, {
        path: workspaceSettingsPath,
        settings: workspaceSettings,
        originalSettings: workspaceOriginalSettings,
        rawJson: workspaceResult.rawJson,
    }, isTrusted, migratedInMemorScopes);
}
export function migrateDeprecatedSettings(loadedSettings, workspaceDir = process.cwd()) {
    const processScope = (scope) => {
        const settings = loadedSettings.forScope(scope).settings;
        if (settings.extensions?.disabled) {
            console.log(`Migrating deprecated extensions.disabled settings from ${scope} settings...`);
            for (const extension of settings.extensions.disabled ?? []) {
                disableExtension(extension, scope, workspaceDir);
            }
            const newExtensionsValue = { ...settings.extensions };
            newExtensionsValue.disabled = undefined;
            loadedSettings.setValue(scope, 'extensions', newExtensionsValue);
        }
    };
    processScope(SettingScope.User);
    processScope(SettingScope.Workspace);
}
export function saveSettings(settingsFile) {
    try {
        // Ensure the directory exists
        const dirPath = path.dirname(settingsFile.path);
        if (!fs.existsSync(dirPath)) {
            fs.mkdirSync(dirPath, { recursive: true });
        }
        let settingsToSave = settingsFile.originalSettings;
        if (!MIGRATE_V2_OVERWRITE) {
            settingsToSave = migrateSettingsToV1(settingsToSave);
        }
        // Use the format-preserving update function
        updateSettingsFilePreservingFormat(settingsFile.path, settingsToSave);
    }
    catch (error) {
        console.error('Error saving user settings file:', error);
    }
}
//# sourceMappingURL=settings.js.map