2025-05-01 10:34:07 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
import * as fs from 'fs' ;
import * as path from 'path' ;
2025-07-09 21:16:42 +00:00
import { homedir , platform } from 'os' ;
2025-07-07 01:13:13 -04:00
import * as dotenv from 'dotenv' ;
2025-06-14 00:00:24 -07:00
import {
2025-07-07 01:13:13 -04:00
GEMINI_CONFIG_DIR as GEMINI_DIR ,
2025-06-14 00:00:24 -07:00
getErrorMessage ,
2025-08-20 10:55:47 +09:00
Storage ,
2025-06-25 05:41:11 -07:00
} from '@google/gemini-cli-core' ;
2025-05-18 10:47:57 -07:00
import stripJsonComments from 'strip-json-comments' ;
2025-05-31 11:10:52 -07:00
import { DefaultLight } from '../ui/themes/default-light.js' ;
import { DefaultDark } from '../ui/themes/default.js' ;
2025-08-21 00:38:12 -07:00
import { isWorkspaceTrusted } from './trustedFolders.js' ;
2025-08-10 09:04:52 +09:00
import { Settings , MemoryImportFormat } from './settingsSchema.js' ;
export type { Settings , MemoryImportFormat } ;
2025-05-01 10:34:07 -07:00
2025-05-01 12:08:24 -07:00
export const SETTINGS_DIRECTORY_NAME = '.gemini' ;
2025-08-20 10:55:47 +09:00
export const USER_SETTINGS_PATH = Storage . getGlobalSettingsPath ( ) ;
export const USER_SETTINGS_DIR = path . dirname ( USER_SETTINGS_PATH ) ;
2025-08-03 20:44:15 +02:00
export const DEFAULT_EXCLUDED_ENV_VARS = [ 'DEBUG' , 'DEBUG_MODE' ] ;
2025-05-01 10:34:07 -07:00
2025-07-21 20:14:07 +00:00
export function getSystemSettingsPath ( ) : string {
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GEMINI_CLI_SYSTEM_SETTINGS_PATH' ] ) {
return process . env [ 'GEMINI_CLI_SYSTEM_SETTINGS_PATH' ] ;
2025-07-21 20:14:07 +00:00
}
2025-07-09 21:16:42 +00:00
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' ;
}
}
2025-08-10 09:04:52 +09:00
export type { DnsResolutionOrder } from './settingsSchema.js' ;
2025-08-01 12:30:39 -07:00
2025-05-01 10:34:07 -07:00
export enum SettingScope {
User = 'User' ,
Workspace = 'Workspace' ,
2025-07-09 21:16:42 +00:00
System = 'System' ,
2025-05-01 10:34:07 -07:00
}
2025-06-20 00:39:15 -04:00
export interface CheckpointingSettings {
enabled? : boolean ;
}
2025-07-15 10:22:31 -07:00
export interface SummarizeToolOutputSettings {
tokenBudget? : number ;
}
2025-06-04 00:46:57 -07:00
export interface AccessibilitySettings {
disableLoadingPhrases? : boolean ;
2025-08-21 22:29:15 +00:00
screenReader? : boolean ;
2025-06-04 00:46:57 -07:00
}
2025-06-06 09:56:45 -07:00
export interface SettingsError {
message : string ;
path : string ;
}
2025-05-01 10:34:07 -07:00
export interface SettingsFile {
settings : Settings ;
path : string ;
}
2025-08-19 13:07:42 -06:00
function mergeSettings (
system : Settings ,
user : Settings ,
workspace : Settings ,
2025-08-21 00:38:12 -07:00
isTrusted : boolean ,
2025-08-19 13:07:42 -06:00
) : Settings {
2025-08-21 00:38:12 -07:00
const safeWorkspace = isTrusted ? workspace : ( { } as Settings ) ;
2025-08-19 13:07:42 -06:00
// folderTrust is not supported at workspace level.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2025-08-21 00:38:12 -07:00
const { folderTrust , . . . safeWorkspaceWithoutFolderTrust } = safeWorkspace ;
2025-08-19 13:07:42 -06:00
return {
. . . user ,
2025-08-21 00:38:12 -07:00
. . . safeWorkspaceWithoutFolderTrust ,
2025-08-19 13:07:42 -06:00
. . . system ,
customThemes : {
. . . ( user . customThemes || { } ) ,
2025-08-21 00:38:12 -07:00
. . . ( safeWorkspace . customThemes || { } ) ,
2025-08-19 13:07:42 -06:00
. . . ( system . customThemes || { } ) ,
} ,
mcpServers : {
. . . ( user . mcpServers || { } ) ,
2025-08-21 00:38:12 -07:00
. . . ( safeWorkspace . mcpServers || { } ) ,
2025-08-19 13:07:42 -06:00
. . . ( system . mcpServers || { } ) ,
} ,
includeDirectories : [
. . . ( system . includeDirectories || [ ] ) ,
. . . ( user . includeDirectories || [ ] ) ,
2025-08-21 00:38:12 -07:00
. . . ( safeWorkspace . includeDirectories || [ ] ) ,
2025-08-19 13:07:42 -06:00
] ,
chatCompression : {
. . . ( system . chatCompression || { } ) ,
. . . ( user . chatCompression || { } ) ,
2025-08-21 00:38:12 -07:00
. . . ( safeWorkspace . chatCompression || { } ) ,
2025-08-19 13:07:42 -06:00
} ,
} ;
}
2025-05-01 10:34:07 -07:00
export class LoadedSettings {
2025-06-06 09:56:45 -07:00
constructor (
2025-07-09 21:16:42 +00:00
system : SettingsFile ,
2025-06-06 09:56:45 -07:00
user : SettingsFile ,
workspace : SettingsFile ,
errors : SettingsError [ ] ,
2025-08-21 00:38:12 -07:00
isTrusted : boolean ,
2025-06-06 09:56:45 -07:00
) {
2025-07-09 21:16:42 +00:00
this . system = system ;
2025-05-01 10:34:07 -07:00
this . user = user ;
this . workspace = workspace ;
2025-06-06 09:56:45 -07:00
this . errors = errors ;
2025-08-21 00:38:12 -07:00
this . isTrusted = isTrusted ;
2025-05-02 08:15:46 -07:00
this . _merged = this . computeMergedSettings ( ) ;
2025-05-01 10:34:07 -07:00
}
2025-07-09 21:16:42 +00:00
readonly system : SettingsFile ;
2025-05-01 10:34:07 -07:00
readonly user : SettingsFile ;
readonly workspace : SettingsFile ;
2025-06-06 09:56:45 -07:00
readonly errors : SettingsError [ ] ;
2025-08-21 00:38:12 -07:00
readonly isTrusted : boolean ;
2025-05-01 10:34:07 -07:00
2025-05-02 08:15:46 -07:00
private _merged : Settings ;
2025-05-01 10:34:07 -07:00
2025-05-02 08:15:46 -07:00
get merged ( ) : Settings {
return this . _merged ;
2025-05-01 10:34:07 -07:00
}
private computeMergedSettings ( ) : Settings {
2025-08-19 13:07:42 -06:00
return mergeSettings (
this . system . settings ,
this . user . settings ,
this . workspace . settings ,
2025-08-21 00:38:12 -07:00
this . isTrusted ,
2025-08-19 13:07:42 -06:00
) ;
2025-05-01 10:34:07 -07:00
}
forScope ( scope : SettingScope ) : SettingsFile {
switch ( scope ) {
case SettingScope . User :
return this . user ;
case SettingScope . Workspace :
return this . workspace ;
2025-07-09 21:16:42 +00:00
case SettingScope . System :
return this . system ;
2025-05-01 10:34:07 -07:00
default :
throw new Error ( ` Invalid scope: ${ scope } ` ) ;
}
}
2025-07-20 16:51:18 +09:00
setValue < K extends keyof Settings > (
2025-05-01 10:34:07 -07:00
scope : SettingScope ,
2025-07-20 16:51:18 +09:00
key : K ,
value : Settings [ K ] ,
2025-05-01 10:34:07 -07:00
) : void {
const settingsFile = this . forScope ( scope ) ;
settingsFile . settings [ key ] = value ;
2025-05-02 08:15:46 -07:00
this . _merged = this . computeMergedSettings ( ) ;
2025-05-01 10:34:07 -07:00
saveSettings ( settingsFile ) ;
}
}
2025-06-06 13:54:59 +00:00
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 {
2025-06-06 15:32:39 +00:00
if (
obj === null ||
obj === undefined ||
typeof obj === 'boolean' ||
typeof obj === 'number'
) {
return obj ;
}
2025-06-06 13:54:59 +00:00
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 ;
}
2025-06-06 15:32:39 +00:00
if ( typeof obj === 'object' ) {
2025-06-06 13:54:59 +00:00
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 ;
}
2025-07-07 01:13:13 -04:00
function findEnvFile ( startDir : string ) : string | null {
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 ;
}
}
2025-07-07 15:02:13 -07:00
export function setUpCloudShellEnvironment ( envFilePath : string | null ) : void {
// 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 ) ;
2025-08-17 12:43:21 -04:00
if ( parsedEnv [ 'GOOGLE_CLOUD_PROJECT' ] ) {
2025-07-07 15:02:13 -07:00
// .env file takes precedence in Cloud Shell
2025-08-17 12:43:21 -04:00
process . env [ 'GOOGLE_CLOUD_PROJECT' ] = parsedEnv [ 'GOOGLE_CLOUD_PROJECT' ] ;
2025-07-07 15:02:13 -07:00
} else {
// If not in .env, set to default and override global
2025-08-17 12:43:21 -04:00
process . env [ 'GOOGLE_CLOUD_PROJECT' ] = 'cloudshell-gca' ;
2025-07-07 15:02:13 -07:00
}
} else {
// If no .env file, set to default and override global
2025-08-17 12:43:21 -04:00
process . env [ 'GOOGLE_CLOUD_PROJECT' ] = 'cloudshell-gca' ;
2025-07-07 15:02:13 -07:00
}
}
2025-08-03 20:44:15 +02:00
export function loadEnvironment ( settings? : Settings ) : void {
2025-07-07 01:13:13 -04:00
const envFilePath = findEnvFile ( process . cwd ( ) ) ;
2025-07-07 15:02:13 -07:00
2025-08-03 20:44:15 +02:00
// Cloud Shell environment variable handling
2025-08-17 12:43:21 -04:00
if ( process . env [ 'CLOUD_SHELL' ] === 'true' ) {
2025-07-07 15:02:13 -07:00
setUpCloudShellEnvironment ( envFilePath ) ;
}
2025-08-03 20:44:15 +02:00
// If no settings provided, try to load workspace settings for exclusions
let resolvedSettings = settings ;
if ( ! resolvedSettings ) {
2025-08-20 10:55:47 +09:00
const workspaceSettingsPath = new Storage (
process . cwd ( ) ,
) . getWorkspaceSettingsPath ( ) ;
2025-08-03 20:44:15 +02:00
try {
if ( fs . existsSync ( workspaceSettingsPath ) ) {
const workspaceContent = fs . readFileSync (
workspaceSettingsPath ,
'utf-8' ,
) ;
const parsedWorkspaceSettings = JSON . parse (
stripJsonComments ( workspaceContent ) ,
) as Settings ;
resolvedSettings = resolveEnvVarsInObject ( parsedWorkspaceSettings ) ;
}
} catch ( _e ) {
// Ignore errors loading workspace settings
}
}
2025-07-07 01:13:13 -04:00
if ( envFilePath ) {
2025-08-03 20:44:15 +02:00
// 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 =
resolvedSettings ? . excludedProjectEnvVars || 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 })`.
}
2025-07-07 01:13:13 -04:00
}
}
2025-05-01 10:34:07 -07:00
/ * *
2025-05-02 08:15:46 -07:00
* Loads settings from user and workspace directories .
2025-05-01 10:34:07 -07:00
* Project settings override user settings .
* /
2025-05-02 08:15:46 -07:00
export function loadSettings ( workspaceDir : string ) : LoadedSettings {
2025-07-09 21:16:42 +00:00
let systemSettings : Settings = { } ;
2025-05-01 10:34:07 -07:00
let userSettings : Settings = { } ;
2025-05-31 11:10:52 -07:00
let workspaceSettings : Settings = { } ;
2025-06-06 09:56:45 -07:00
const settingsErrors : SettingsError [ ] = [ ] ;
2025-07-21 20:14:07 +00:00
const systemSettingsPath = getSystemSettingsPath ( ) ;
2025-08-02 03:52:17 +05:30
2025-08-05 21:10:16 +02:00
// Resolve paths to their canonical representation to handle symlinks
2025-08-02 03:52:17 +05:30
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 ) ;
2025-08-20 10:55:47 +09:00
const workspaceSettingsPath = new Storage (
workspaceDir ,
) . getWorkspaceSettingsPath ( ) ;
2025-08-03 20:44:15 +02:00
2025-07-09 21:16:42 +00:00
// Load system settings
try {
2025-07-21 20:14:07 +00:00
if ( fs . existsSync ( systemSettingsPath ) ) {
const systemContent = fs . readFileSync ( systemSettingsPath , 'utf-8' ) ;
2025-08-19 13:07:42 -06:00
systemSettings = JSON . parse ( stripJsonComments ( systemContent ) ) as Settings ;
2025-07-09 21:16:42 +00:00
}
} catch ( error : unknown ) {
settingsErrors . push ( {
message : getErrorMessage ( error ) ,
2025-07-21 20:14:07 +00:00
path : systemSettingsPath ,
2025-07-09 21:16:42 +00:00
} ) ;
}
2025-05-01 10:34:07 -07:00
// Load user settings
try {
if ( fs . existsSync ( USER_SETTINGS_PATH ) ) {
const userContent = fs . readFileSync ( USER_SETTINGS_PATH , 'utf-8' ) ;
2025-08-19 13:07:42 -06:00
userSettings = JSON . parse ( stripJsonComments ( userContent ) ) as Settings ;
2025-05-31 11:10:52 -07:00
// 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 ;
}
2025-05-01 10:34:07 -07:00
}
2025-06-06 09:56:45 -07:00
} catch ( error : unknown ) {
settingsErrors . push ( {
message : getErrorMessage ( error ) ,
path : USER_SETTINGS_PATH ,
} ) ;
2025-05-01 10:34:07 -07:00
}
2025-08-02 03:52:17 +05:30
if ( realWorkspaceDir !== realHomeDir ) {
// Load workspace settings
try {
if ( fs . existsSync ( workspaceSettingsPath ) ) {
const projectContent = fs . readFileSync ( workspaceSettingsPath , 'utf-8' ) ;
2025-08-19 13:07:42 -06:00
workspaceSettings = JSON . parse (
2025-08-02 03:52:17 +05:30
stripJsonComments ( projectContent ) ,
) as Settings ;
if ( workspaceSettings . theme && workspaceSettings . theme === 'VS' ) {
workspaceSettings . theme = DefaultLight . name ;
} else if (
workspaceSettings . theme &&
workspaceSettings . theme === 'VS2015'
) {
workspaceSettings . theme = DefaultDark . name ;
}
2025-05-31 11:10:52 -07:00
}
2025-08-02 03:52:17 +05:30
} catch ( error : unknown ) {
settingsErrors . push ( {
message : getErrorMessage ( error ) ,
path : workspaceSettingsPath ,
} ) ;
2025-05-01 10:34:07 -07:00
}
}
2025-08-21 00:38:12 -07:00
// For the initial trust check, we can only use user and system settings.
const initialTrustCheckSettings = { . . . systemSettings , . . . userSettings } ;
const isTrusted = isWorkspaceTrusted ( initialTrustCheckSettings ) ? ? true ;
2025-08-19 13:07:42 -06:00
// Create a temporary merged settings object to pass to loadEnvironment.
const tempMergedSettings = mergeSettings (
systemSettings ,
userSettings ,
workspaceSettings ,
2025-08-21 00:38:12 -07:00
isTrusted ,
2025-08-19 13:07:42 -06:00
) ;
// loadEnviroment depends on settings so we have to create a temp version of
// the settings to avoid a cycle
loadEnvironment ( tempMergedSettings ) ;
// Now that the environment is loaded, resolve variables in the settings.
systemSettings = resolveEnvVarsInObject ( systemSettings ) ;
userSettings = resolveEnvVarsInObject ( userSettings ) ;
workspaceSettings = resolveEnvVarsInObject ( workspaceSettings ) ;
2025-08-03 20:44:15 +02:00
// Create LoadedSettings first
const loadedSettings = new LoadedSettings (
2025-07-09 21:16:42 +00:00
{
2025-07-21 20:14:07 +00:00
path : systemSettingsPath ,
2025-07-09 21:16:42 +00:00
settings : systemSettings ,
} ,
2025-06-06 09:56:45 -07:00
{
path : USER_SETTINGS_PATH ,
settings : userSettings ,
} ,
{
path : workspaceSettingsPath ,
settings : workspaceSettings ,
} ,
settingsErrors ,
2025-08-21 00:38:12 -07:00
isTrusted ,
2025-05-01 10:34:07 -07:00
) ;
2025-08-03 20:44:15 +02:00
2025-08-07 07:34:40 -07:00
// Validate chatCompression settings
const chatCompression = loadedSettings . merged . chatCompression ;
const threshold = chatCompression ? . contextPercentageThreshold ;
if (
threshold != null &&
( typeof threshold !== 'number' || threshold < 0 || threshold > 1 )
) {
console . warn (
` Invalid value for chatCompression.contextPercentageThreshold: " ${ threshold } ". Please use a value between 0 and 1. Using default compression settings. ` ,
) ;
delete loadedSettings . merged . chatCompression ;
}
2025-08-03 20:44:15 +02:00
return loadedSettings ;
2025-05-01 10:34:07 -07:00
}
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 ) ;
}
}