|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
fileFiltering?: { |
|
|
respectGitIgnore?: boolean; |
|
|
enableRecursiveFileSearch?: boolean; |
|
|
}; |
|
|
|
|
|
|
|
|
hideWindowTitle?: boolean; |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
settingsFile.settings[key] = value; |
|
|
this._merged = this.computeMergedSettings(); |
|
|
saveSettings(settingsFile); |
|
|
} |
|
|
} |
|
|
|
|
|
function resolveEnvVarsInString(value: string): string { |
|
|
const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; |
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function loadSettings(workspaceDir: string): LoadedSettings { |
|
|
let userSettings: Settings = {}; |
|
|
let workspaceSettings: Settings = {}; |
|
|
const settingsErrors: SettingsError[] = []; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
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', |
|
|
); |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|