packages/cli/src/config/settings.ts (218 lines of code) (raw):

/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import * as fs from 'fs'; import * as path from 'path'; import { homedir } from 'os'; import { MCPServerConfig, getErrorMessage, BugCommandSettings, TelemetrySettings, AuthType, } 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'; export const SETTINGS_DIRECTORY_NAME = '.gemini'; export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME); export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json'); export enum SettingScope { User = 'User', Workspace = 'Workspace', } export interface CheckpointingSettings { enabled?: boolean; } export interface AccessibilitySettings { disableLoadingPhrases?: boolean; } export interface Settings { theme?: string; selectedAuthType?: AuthType; sandbox?: boolean | string; coreTools?: string[]; excludeTools?: string[]; toolDiscoveryCommand?: string; toolCallCommand?: string; mcpServerCommand?: string; mcpServers?: Record<string, MCPServerConfig>; showMemoryUsage?: boolean; contextFileName?: string | string[]; accessibility?: AccessibilitySettings; telemetry?: TelemetrySettings; usageStatisticsEnabled?: boolean; preferredEditor?: string; bugCommand?: BugCommandSettings; checkpointing?: CheckpointingSettings; autoConfigureMaxOldSpaceSize?: boolean; // Git-aware file filtering settings fileFiltering?: { respectGitIgnore?: boolean; enableRecursiveFileSearch?: boolean; }; // UI setting. Does not display the ANSI-controlled terminal title. hideWindowTitle?: boolean; hideTips?: boolean; // Add other settings here. } export interface SettingsError { message: string; path: string; } export interface SettingsFile { settings: Settings; path: string; } export class LoadedSettings { constructor( user: SettingsFile, workspace: SettingsFile, errors: SettingsError[], ) { this.user = user; this.workspace = workspace; this.errors = errors; this._merged = this.computeMergedSettings(); } readonly user: SettingsFile; readonly workspace: SettingsFile; readonly errors: SettingsError[]; private _merged: Settings; get merged(): Settings { return this._merged; } private computeMergedSettings(): Settings { return { ...this.user.settings, ...this.workspace.settings, }; } forScope(scope: SettingScope): SettingsFile { switch (scope) { case SettingScope.User: return this.user; case SettingScope.Workspace: return this.workspace; default: throw new Error(`Invalid scope: ${scope}`); } } setValue( scope: SettingScope, key: keyof Settings, value: string | Record<string, MCPServerConfig> | undefined, ): void { const settingsFile = this.forScope(scope); // @ts-expect-error - value can be string | Record<string, MCPServerConfig> settingsFile.settings[key] = value; this._merged = this.computeMergedSettings(); saveSettings(settingsFile); } } function resolveEnvVarsInString(value: string): string { const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME} return value.replace(envVarRegex, (match, varName1, varName2) => { const varName = varName1 || varName2; if (process && process.env && typeof process.env[varName] === 'string') { return process.env[varName]!; } return match; }); } function resolveEnvVarsInObject<T>(obj: T): T { if ( obj === null || obj === undefined || typeof obj === 'boolean' || typeof obj === 'number' ) { return obj; } if (typeof obj === 'string') { return resolveEnvVarsInString(obj) as unknown as T; } if (Array.isArray(obj)) { return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T; } if (typeof obj === 'object') { const newObj = { ...obj } as T; for (const key in newObj) { if (Object.prototype.hasOwnProperty.call(newObj, key)) { newObj[key] = resolveEnvVarsInObject(newObj[key]); } } return newObj; } return obj; } /** * Loads settings from user and workspace directories. * Project settings override user settings. */ export function loadSettings(workspaceDir: string): LoadedSettings { let userSettings: Settings = {}; let workspaceSettings: Settings = {}; const settingsErrors: SettingsError[] = []; // Load user settings try { if (fs.existsSync(USER_SETTINGS_PATH)) { const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8'); const parsedUserSettings = JSON.parse( stripJsonComments(userContent), ) as Settings; userSettings = resolveEnvVarsInObject(parsedUserSettings); // Support legacy theme names if (userSettings.theme && userSettings.theme === 'VS') { userSettings.theme = DefaultLight.name; } else if (userSettings.theme && userSettings.theme === 'VS2015') { userSettings.theme = DefaultDark.name; } } } catch (error: unknown) { settingsErrors.push({ message: getErrorMessage(error), path: USER_SETTINGS_PATH, }); } const workspaceSettingsPath = path.join( workspaceDir, SETTINGS_DIRECTORY_NAME, 'settings.json', ); // Load workspace settings try { if (fs.existsSync(workspaceSettingsPath)) { const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8'); const parsedWorkspaceSettings = JSON.parse( stripJsonComments(projectContent), ) as Settings; workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings); if (workspaceSettings.theme && workspaceSettings.theme === 'VS') { workspaceSettings.theme = DefaultLight.name; } else if ( workspaceSettings.theme && workspaceSettings.theme === 'VS2015' ) { workspaceSettings.theme = DefaultDark.name; } } } catch (error: unknown) { settingsErrors.push({ message: getErrorMessage(error), path: workspaceSettingsPath, }); } return new LoadedSettings( { path: USER_SETTINGS_PATH, settings: userSettings, }, { path: workspaceSettingsPath, settings: workspaceSettings, }, settingsErrors, ); } export function saveSettings(settingsFile: SettingsFile): void { try { // Ensure the directory exists const dirPath = path.dirname(settingsFile.path); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } fs.writeFileSync( settingsFile.path, JSON.stringify(settingsFile.settings, null, 2), 'utf-8', ); } catch (error) { console.error('Error saving user settings file:', error); } }