Download packages/cli/src/config/settings.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 43.5 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/config/settings.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/config/settings.ts
-
curl -L -o settings.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/config/settings.ts
43.5 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import * as fs from 'node:fs'; | |
| import * as path from 'node:path'; | |
| import { platform } from 'node:os'; | |
| import * as dotenv from 'dotenv'; | |
| import process from 'node:process'; | |
| import { | |
| CoreEvent, | |
| FatalConfigError, | |
| GEMINI_DIR, | |
| getErrorMessage, | |
| getFsErrorMessage, | |
| Storage, | |
| coreEvents, | |
| homedir, | |
| AuthType, | |
| type AdminControlsSettings, | |
| createCache, | |
| isFileAndDirectorySecureSync, | |
| createPathSecurityCache, | |
| } from '@google/gemini-cli-core'; | |
| import stripJsonComments from 'strip-json-comments'; | |
| import { DefaultLight } from '../ui/themes/builtin/light/default-light.js'; | |
| import { DefaultDark } from '../ui/themes/builtin/dark/default-dark.js'; | |
| import { isWorkspaceTrusted } from './trustedFolders.js'; | |
| import { | |
| type Settings, | |
| type MergedSettings, | |
| type MemoryImportFormat, | |
| type MergeStrategy, | |
| type SettingsSchema, | |
| type SettingDefinition, | |
| getSettingsSchema, | |
| } from './settingsSchema.js'; | |
| export { | |
| type Settings, | |
| type MergedSettings, | |
| type MemoryImportFormat, | |
| type MergeStrategy, | |
| type SettingsSchema, | |
| type SettingDefinition, | |
| getSettingsSchema, | |
| }; | |
| import { resolveEnvVarsInObject } from '../utils/envVarResolver.js'; | |
| import { customDeepMerge } from '../utils/deepMerge.js'; | |
| import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js'; | |
| import { | |
| validateSettings, | |
| formatValidationError, | |
| } from './settings-validation.js'; | |
| export function getMergeStrategyForPath( | |
| path: string[], | |
| ): MergeStrategy | undefined { | |
| let current: SettingDefinition | undefined = undefined; | |
| let currentSchema: SettingsSchema | undefined = getSettingsSchema(); | |
| let parent: SettingDefinition | undefined = undefined; | |
| for (const key of path) { | |
| if (!currentSchema || !currentSchema[key]) { | |
| // Key not found in schema - check if parent has additionalProperties | |
| if (parent?.additionalProperties?.mergeStrategy) { | |
| return parent.additionalProperties.mergeStrategy; | |
| } | |
| return undefined; | |
| } | |
| parent = current; | |
| current = currentSchema[key]; | |
| currentSchema = current.properties; | |
| } | |
| return current?.mergeStrategy; | |
| } | |
| 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', | |
| 'GEMINI_CLI_IDE_SERVER_STDIO_COMMAND', | |
| 'GEMINI_CLI_IDE_SERVER_STDIO_ARGS', | |
| ]; | |
| const AUTH_ENV_VAR_WHITELIST = [ | |
| 'GEMINI_API_KEY', | |
| 'GOOGLE_API_KEY', | |
| 'GOOGLE_CLOUD_PROJECT', | |
| 'GOOGLE_CLOUD_LOCATION', | |
| ]; | |
| /** | |
| * Sanitizes an environment variable value to prevent shell injection. | |
| * Restricts values to a safe character set: alphanumeric, -, _, ., / | |
| */ | |
| export function sanitizeEnvVar(value: string): string { | |
| return value.replace(/[^a-zA-Z0-9\-_./]/g, ''); | |
| } | |
| export function getSystemSettingsPath(): string { | |
| 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(): string { | |
| 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 type { DnsResolutionOrder } from './settingsSchema.js'; | |
| export enum SettingScope { | |
| User = 'User', | |
| Workspace = 'Workspace', | |
| System = 'System', | |
| SystemDefaults = 'SystemDefaults', | |
| // Note that this scope is not supported in the settings dialog at this time, | |
| // it is only supported for extensions. | |
| Session = 'Session', | |
| } | |
| /** | |
| * A type representing the settings scopes that are supported for LoadedSettings. | |
| */ | |
| export type LoadableSettingScope = | |
| | SettingScope.User | |
| | SettingScope.Workspace | |
| | SettingScope.System | |
| | SettingScope.SystemDefaults; | |
| /** | |
| * The actual values of the loadable settings scopes. | |
| */ | |
| const _loadableSettingScopes = [ | |
| SettingScope.User, | |
| SettingScope.Workspace, | |
| SettingScope.System, | |
| SettingScope.SystemDefaults, | |
| ]; | |
| /** | |
| * A type guard function that checks if `scope` is a loadable settings scope, | |
| * and allows promotion to the `LoadableSettingsScope` type based on the result. | |
| */ | |
| export function isLoadableSettingScope( | |
| scope: SettingScope, | |
| ): scope is LoadableSettingScope { | |
| return _loadableSettingScopes.includes(scope); | |
| } | |
| export interface CheckpointingSettings { | |
| enabled?: boolean; | |
| } | |
| export interface SummarizeToolOutputSettings { | |
| tokenBudget?: number; | |
| } | |
| export type LoadingPhrasesMode = 'tips' | 'witty' | 'all' | 'off'; | |
| export interface AccessibilitySettings { | |
| /** @deprecated Use ui.loadingPhrases instead. */ | |
| enableLoadingPhrases?: boolean; | |
| screenReader?: boolean; | |
| } | |
| export interface SessionRetentionSettings { | |
| /** Enable automatic session cleanup */ | |
| enabled?: boolean; | |
| /** Maximum age of sessions to keep (e.g., "30d", "7d", "24h", "1w") */ | |
| maxAge?: string; | |
| /** Alternative: Maximum number of sessions to keep (most recent) */ | |
| maxCount?: number; | |
| /** Minimum retention period (safety limit, defaults to "1d") */ | |
| minRetention?: string; | |
| } | |
| export interface SettingsError { | |
| message: string; | |
| path: string; | |
| severity: 'error' | 'warning'; | |
| } | |
| export interface SettingsFile { | |
| settings: Settings; | |
| originalSettings: Settings; | |
| path: string; | |
| rawJson?: string; | |
| readOnly?: boolean; | |
| } | |
| function setNestedProperty( | |
| obj: Record<string, unknown>, | |
| path: string, | |
| value: unknown, | |
| ) { | |
| const keys = path.split('.'); | |
| const lastKey = keys.pop(); | |
| if (!lastKey) return; | |
| let current: Record<string, unknown> = obj; | |
| for (const key of keys) { | |
| if (current[key] === undefined) { | |
| current[key] = {}; | |
| } | |
| const next = current[key]; | |
| if (typeof next === 'object' && next !== null) { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| current = next as Record<string, unknown>; | |
| } else { | |
| // This path is invalid, so we stop. | |
| return; | |
| } | |
| } | |
| current[lastKey] = value; | |
| } | |
| export function getDefaultsFromSchema( | |
| schema: SettingsSchema = getSettingsSchema(), | |
| ): Settings { | |
| const defaults: Record<string, unknown> = {}; | |
| for (const key in schema) { | |
| const definition = schema[key]; | |
| if (definition.properties) { | |
| defaults[key] = getDefaultsFromSchema(definition.properties); | |
| } else if (definition.default !== undefined) { | |
| defaults[key] = definition.default; | |
| } | |
| } | |
| return defaults as Settings; | |
| } | |
| export function mergeSettings( | |
| system: Settings, | |
| systemDefaults: Settings, | |
| user: Settings, | |
| workspace: Settings, | |
| isTrusted: boolean, | |
| ): MergedSettings { | |
| const safeWorkspace = isTrusted ? workspace : ({} as Settings); | |
| const schemaDefaults = getDefaultsFromSchema(); | |
| // Settings are merged with the following precedence (last one wins for | |
| // single values): | |
| // 1. Schema Defaults (Built-in) | |
| // 2. System Defaults | |
| // 3. User Settings | |
| // 4. Workspace Settings | |
| // 5. System Settings (as overrides) | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| return customDeepMerge( | |
| getMergeStrategyForPath, | |
| schemaDefaults, | |
| systemDefaults, | |
| user, | |
| safeWorkspace, | |
| system, | |
| ) as MergedSettings; | |
| } | |
| /** | |
| * Creates a fully populated MergedSettings object for testing purposes. | |
| * It merges the provided overrides with the default settings from the schema. | |
| * | |
| * @param overrides Partial settings to override the defaults. | |
| * @returns A complete MergedSettings object. | |
| */ | |
| export function createTestMergedSettings( | |
| overrides: Partial<Settings> = {}, | |
| ): MergedSettings { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| return customDeepMerge( | |
| getMergeStrategyForPath, | |
| getDefaultsFromSchema(), | |
| overrides, | |
| ) as MergedSettings; | |
| } | |
| /** | |
| * An immutable snapshot of settings state. | |
| * Used with useSyncExternalStore for reactive updates. | |
| */ | |
| export interface LoadedSettingsSnapshot { | |
| system: SettingsFile; | |
| systemDefaults: SettingsFile; | |
| user: SettingsFile; | |
| workspace: SettingsFile; | |
| isTrusted: boolean; | |
| errors: SettingsError[]; | |
| merged: MergedSettings; | |
| } | |
| export class LoadedSettings { | |
| constructor( | |
| system: SettingsFile, | |
| systemDefaults: SettingsFile, | |
| user: SettingsFile, | |
| workspace: SettingsFile, | |
| isTrusted: boolean, | |
| errors: SettingsError[] = [], | |
| ) { | |
| this.system = system; | |
| this.systemDefaults = systemDefaults; | |
| this.user = user; | |
| this._workspaceFile = workspace; | |
| this.isTrusted = isTrusted; | |
| this.workspace = isTrusted | |
| ? workspace | |
| : this.createEmptyWorkspace(workspace); | |
| this.errors = errors; | |
| this._merged = this.computeMergedSettings(); | |
| this._snapshot = this.computeSnapshot(); | |
| } | |
| readonly system: SettingsFile; | |
| readonly systemDefaults: SettingsFile; | |
| readonly user: SettingsFile; | |
| workspace: SettingsFile; | |
| isTrusted: boolean; | |
| readonly errors: SettingsError[]; | |
| private _workspaceFile: SettingsFile; | |
| private _merged: MergedSettings; | |
| private _snapshot: LoadedSettingsSnapshot; | |
| private _remoteAdminSettings: Partial<Settings> | undefined; | |
| get merged(): MergedSettings { | |
| return this._merged; | |
| } | |
| /** | |
| * Returns a merged settings object as if the folder were trusted. | |
| * This is useful for commands like 'mcp list' that want to show | |
| * what's configured even if it's currently disabled for security reasons. | |
| */ | |
| getMergedSettingsAsIfTrusted(): MergedSettings { | |
| return this.computeMergedSettings(true); | |
| } | |
| setTrusted(isTrusted: boolean): void { | |
| if (this.isTrusted === isTrusted) { | |
| return; | |
| } | |
| this.isTrusted = isTrusted; | |
| this.workspace = isTrusted | |
| ? this._workspaceFile | |
| : this.createEmptyWorkspace(this._workspaceFile); | |
| this._merged = this.computeMergedSettings(); | |
| coreEvents.emitSettingsChanged(); | |
| } | |
| private createEmptyWorkspace(workspace: SettingsFile): SettingsFile { | |
| return { | |
| ...workspace, | |
| settings: {}, | |
| originalSettings: {}, | |
| }; | |
| } | |
| private computeMergedSettings(forceTrusted = false): MergedSettings { | |
| const isTrusted = forceTrusted || this.isTrusted; | |
| const workspace = forceTrusted ? this._workspaceFile : this.workspace; | |
| const merged = mergeSettings( | |
| this.system.settings, | |
| this.systemDefaults.settings, | |
| this.user.settings, | |
| workspace.settings, | |
| isTrusted, | |
| ); | |
| // Remote admin settings always take precedence and file-based admin settings | |
| // are ignored. | |
| const adminSettingSchema = getSettingsSchema().admin; | |
| if (adminSettingSchema?.properties) { | |
| const adminSchema = adminSettingSchema.properties; | |
| const adminDefaults = getDefaultsFromSchema(adminSchema); | |
| // The final admin settings are the defaults overridden by remote settings. | |
| // Any admin settings from files are ignored. | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| merged.admin = customDeepMerge( | |
| (path: string[]) => getMergeStrategyForPath(['admin', ...path]), | |
| adminDefaults, | |
| this._remoteAdminSettings?.admin ?? {}, | |
| ) as MergedSettings['admin']; | |
| } | |
| return merged; | |
| } | |
| private computeSnapshot(): LoadedSettingsSnapshot { | |
| const cloneSettingsFile = (file: SettingsFile): SettingsFile => ({ | |
| ...file, | |
| settings: structuredClone(file.settings), | |
| originalSettings: structuredClone(file.originalSettings), | |
| }); | |
| return { | |
| system: cloneSettingsFile(this.system), | |
| systemDefaults: cloneSettingsFile(this.systemDefaults), | |
| user: cloneSettingsFile(this.user), | |
| workspace: cloneSettingsFile(this.workspace), | |
| isTrusted: this.isTrusted, | |
| errors: [...this.errors], | |
| merged: structuredClone(this._merged), | |
| }; | |
| } | |
| // Passing this along with getSnapshot to useSyncExternalStore allows for idiomatic reactivity on settings changes | |
| // React will pass a listener fn into this subscribe fn | |
| // that listener fn will perform an object identity check on the snapshot and trigger a React re render if the snapshot has changed | |
| subscribe(listener: () => void): () => void { | |
| coreEvents.on(CoreEvent.SettingsChanged, listener); | |
| return () => coreEvents.off(CoreEvent.SettingsChanged, listener); | |
| } | |
| getSnapshot(): LoadedSettingsSnapshot { | |
| return this._snapshot; | |
| } | |
| forScope(scope: LoadableSettingScope): SettingsFile { | |
| 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}`); | |
| } | |
| } | |
| private isPersistable(settingsFile: SettingsFile): boolean { | |
| return !settingsFile.readOnly; | |
| } | |
| setValue(scope: LoadableSettingScope, key: string, value: unknown): void { | |
| const settingsFile = this.forScope(scope); | |
| // Clone value to prevent reference sharing | |
| const valueToSet = | |
| typeof value === 'object' && value !== null | |
| ? structuredClone(value) | |
| : value; | |
| setNestedProperty(settingsFile.settings, key, valueToSet); | |
| if (this.isPersistable(settingsFile)) { | |
| // Use a fresh clone for originalSettings to ensure total independence | |
| setNestedProperty( | |
| settingsFile.originalSettings, | |
| key, | |
| structuredClone(valueToSet), | |
| ); | |
| saveSettings(settingsFile); | |
| } | |
| this._merged = this.computeMergedSettings(); | |
| this._snapshot = this.computeSnapshot(); | |
| coreEvents.emitSettingsChanged(); | |
| } | |
| setRemoteAdminSettings(remoteSettings: AdminControlsSettings): void { | |
| const admin: Settings['admin'] = {}; | |
| const { strictModeDisabled, mcpSetting, cliFeatureSetting } = | |
| remoteSettings; | |
| if (Object.keys(remoteSettings).length === 0) { | |
| this._remoteAdminSettings = { admin }; | |
| this._merged = this.computeMergedSettings(); | |
| return; | |
| } | |
| admin.secureModeEnabled = !strictModeDisabled; | |
| admin.mcp = { | |
| enabled: mcpSetting?.mcpEnabled, | |
| config: mcpSetting?.mcpConfig?.mcpServers, | |
| requiredConfig: mcpSetting?.requiredMcpConfig, | |
| }; | |
| admin.extensions = { | |
| enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled, | |
| }; | |
| admin.skills = { | |
| enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled, | |
| }; | |
| this._remoteAdminSettings = { admin }; | |
| this._merged = this.computeMergedSettings(); | |
| } | |
| /** | |
| * Returns a consolidated list of excluded MCP servers across all settings files. | |
| */ | |
| getConsolidatedExcludedMcpServers(): string[] { | |
| const scopes = [ | |
| this.system, | |
| this.systemDefaults, | |
| this.user, | |
| this.workspace, | |
| ]; | |
| return scopes.flatMap((scope) => { | |
| const excluded = scope?.settings?.mcp?.excluded; | |
| return Array.isArray(excluded) ? excluded : []; | |
| }); | |
| } | |
| /** | |
| * Returns a consolidated list of allowed MCP servers (via intersection of all defined lists). | |
| */ | |
| getConsolidatedAllowedMcpServers(): string[] | undefined { | |
| const scopes = [ | |
| this.system, | |
| this.systemDefaults, | |
| this.user, | |
| this.workspace, | |
| ]; | |
| const definedAllowlists = scopes.flatMap((scope) => { | |
| const allowed = scope?.settings?.mcp?.allowed; | |
| return Array.isArray(allowed) ? [allowed] : []; | |
| }); | |
| if (definedAllowlists.length === 0) { | |
| return undefined; | |
| } | |
| return definedAllowlists.reduce((acc, current) => { | |
| const normalizedCurrent = new Set( | |
| current.map((item) => item.toLowerCase().trim()), | |
| ); | |
| return acc.filter((item) => | |
| normalizedCurrent.has(item.toLowerCase().trim()), | |
| ); | |
| }); | |
| } | |
| } | |
| function findEnvFile( | |
| startDir: string, | |
| isTrusted: boolean, | |
| ignoreLocalEnv: boolean, | |
| ): string | null { | |
| let currentDir = path.resolve(startDir); | |
| while (true) { | |
| // prefer gemini-specific .env under GEMINI_DIR | |
| if (isTrusted) { | |
| const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env'); | |
| if (fs.existsSync(geminiEnvPath)) { | |
| return geminiEnvPath; | |
| } | |
| } | |
| const envPath = path.join(currentDir, '.env'); | |
| if (fs.existsSync(envPath)) { | |
| if (!ignoreLocalEnv || currentDir === homedir()) { | |
| return envPath; | |
| } | |
| } | |
| const parentDir = path.dirname(currentDir); | |
| if (parentDir === currentDir || !parentDir) { | |
| // check .env under home as fallback, again preferring gemini-specific .env | |
| if (isTrusted) { | |
| 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; | |
| } | |
| } | |
| // Internal env var used to preserve the user's original GOOGLE_CLOUD_PROJECT | |
| // across process restarts in Cloud Shell. This survives relaunch because child | |
| // processes inherit the parent's environment. | |
| const USER_GCP_PROJECT = '_GEMINI_USER_GCP_PROJECT'; | |
| export function setUpCloudShellEnvironment( | |
| envFilePath: string | null, | |
| isTrusted: boolean, | |
| isSandboxed: boolean, | |
| selectedAuthType?: string, | |
| ): 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. | |
| // | |
| // However, if the user has explicitly selected Vertex AI auth, they intend | |
| // to use their own GCP project, so we restore the original value and skip | |
| // the Cloud Shell override to respect their .env settings. | |
| // Save the user's original value before overwriting, so it can be restored | |
| // if the user later switches to Vertex AI (even after a process restart). | |
| if (!process.env[USER_GCP_PROJECT]) { | |
| const current = process.env['GOOGLE_CLOUD_PROJECT']; | |
| if (current && current !== 'cloudshell-gca') { | |
| process.env[USER_GCP_PROJECT] = current; | |
| } | |
| } | |
| let value: string | undefined = 'cloudshell-gca'; | |
| if (selectedAuthType === AuthType.USE_VERTEX_AI) { | |
| value = process.env[USER_GCP_PROJECT]; | |
| } | |
| 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 | |
| value = parsedEnv['GOOGLE_CLOUD_PROJECT']; | |
| if (!isTrusted && isSandboxed) { | |
| value = sanitizeEnvVar(value); | |
| } | |
| } | |
| } | |
| if (value !== undefined) { | |
| process.env['GOOGLE_CLOUD_PROJECT'] = value; | |
| } else if (process.env['GOOGLE_CLOUD_PROJECT'] === 'cloudshell-gca') { | |
| delete process.env['GOOGLE_CLOUD_PROJECT']; | |
| } | |
| } | |
| export function loadEnvironment( | |
| settings: Settings, | |
| workspaceDir: string, | |
| isWorkspaceTrustedFn = isWorkspaceTrusted, | |
| ): void { | |
| const trustResult = isWorkspaceTrustedFn(settings, workspaceDir); | |
| const isTrusted = trustResult.isTrusted ?? false; | |
| // Check settings OR check process.argv directly since this might be called | |
| // before arguments are fully parsed. This is a best-effort sniffing approach | |
| // that happens early in the CLI lifecycle. It is designed to detect the | |
| // sandbox flag before the full command-line parser is initialized to ensure | |
| // security constraints are applied when loading environment variables. | |
| const args = process.argv.slice(2); | |
| const doubleDashIndex = args.indexOf('--'); | |
| const relevantArgs = | |
| doubleDashIndex === -1 ? args : args.slice(0, doubleDashIndex); | |
| const isSandboxed = | |
| !!settings.tools?.sandbox || | |
| relevantArgs.includes('-s') || | |
| relevantArgs.includes('--sandbox'); | |
| const shouldIgnoreEnv = | |
| !!settings.advanced?.ignoreLocalEnv || | |
| relevantArgs.includes('--ignore-env'); | |
| const envFilePath = findEnvFile(workspaceDir, isTrusted, shouldIgnoreEnv); | |
| // Cloud Shell environment variable handling | |
| if (process.env['CLOUD_SHELL'] === 'true') { | |
| const selectedAuthType = settings.security?.auth?.selectedType; | |
| setUpCloudShellEnvironment( | |
| envFilePath, | |
| isTrusted, | |
| isSandboxed, | |
| selectedAuthType, | |
| ); | |
| } | |
| 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)) { | |
| let value = parsedEnv[key]; | |
| // If the workspace is untrusted, only allow whitelisted variables. | |
| if (!isTrusted) { | |
| if (!AUTH_ENV_VAR_WHITELIST.includes(key)) { | |
| continue; | |
| } | |
| // Sanitize the value for untrusted sources | |
| value = sanitizeEnvVar(value); | |
| } | |
| // 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] = value; | |
| } | |
| } | |
| } | |
| } catch { | |
| // Errors are ignored to match the behavior of `dotenv.config({ quiet: true })`. | |
| } | |
| } | |
| } | |
| // Cache to store the results of loadSettings to avoid redundant disk I/O. | |
| const settingsCache = createCache<string, LoadedSettings>({ | |
| storage: 'map', | |
| defaultTtl: 10000, // 10 seconds | |
| }); | |
| /** | |
| * Resets the settings cache. Used exclusively for test isolation. | |
| * @internal | |
| */ | |
| export function resetSettingsCacheForTesting() { | |
| settingsCache.clear(); | |
| } | |
| export function isWorktreeEnabled(settings: LoadedSettings): boolean { | |
| return settings.merged.experimental.worktrees; | |
| } | |
| /** | |
| * Loads settings from user and workspace directories. | |
| * Project settings override user settings. | |
| */ | |
| export function loadSettings( | |
| workspaceDir: string = process.cwd(), | |
| ): LoadedSettings { | |
| const normalizedWorkspaceDir = path.resolve(workspaceDir); | |
| return settingsCache.getOrCreate(normalizedWorkspaceDir, () => | |
| _doLoadSettings(normalizedWorkspaceDir), | |
| ); | |
| } | |
| /** | |
| * Internal implementation of the settings loading logic. | |
| */ | |
| function _doLoadSettings(workspaceDir: string): LoadedSettings { | |
| let systemSettings: Settings = {}; | |
| let systemDefaultSettings: Settings = {}; | |
| let userSettings: Settings = {}; | |
| let workspaceSettings: Settings = {}; | |
| const settingsErrors: SettingsError[] = []; | |
| const systemSettingsPath = getSystemSettingsPath(); | |
| const systemDefaultsPath = getSystemDefaultsPath(); | |
| const storage = new Storage(workspaceDir); | |
| const workspaceSettingsPath = storage.getWorkspaceSettingsPath(); | |
| const load = ( | |
| filePath: string, | |
| ): { settings: Settings; rawSettings: Settings; rawJson?: string } => { | |
| try { | |
| if (fs.existsSync(filePath)) { | |
| const content = fs.readFileSync(filePath, 'utf-8'); | |
| const rawSettings: unknown = 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, | |
| severity: 'error', | |
| }); | |
| return { settings: {}, rawSettings: {} }; | |
| } | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| const settingsObject = rawSettings as Record<string, unknown>; | |
| // Expand environment variables | |
| const expandedSettings = resolveEnvVarsInObject( | |
| settingsObject as Settings, | |
| ); | |
| // Validate settings structure with Zod after environment variable expansion | |
| const validationResult = validateSettings(expandedSettings); | |
| if (!validationResult.success && validationResult.error) { | |
| const errorMessage = formatValidationError( | |
| validationResult.error, | |
| filePath, | |
| ); | |
| settingsErrors.push({ | |
| message: errorMessage, | |
| path: filePath, | |
| severity: 'warning', | |
| }); | |
| return { | |
| settings: expandedSettings, | |
| rawSettings: settingsObject as Settings, | |
| rawJson: content, | |
| }; | |
| } | |
| // Return the successfully cast and validated data | |
| return { | |
| // Since we've successfully validated expandedSettings against settingsZodSchema, | |
| // it's safe to cast the resulting data to the Settings type. | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| settings: (validationResult.data as Settings) ?? expandedSettings, | |
| rawSettings: settingsObject as Settings, | |
| rawJson: content, | |
| }; | |
| } | |
| } catch (error: unknown) { | |
| settingsErrors.push({ | |
| message: getErrorMessage(error), | |
| path: filePath, | |
| severity: 'error', | |
| }); | |
| } | |
| return { settings: {}, rawSettings: {} }; | |
| }; | |
| const securityCache = createPathSecurityCache(); | |
| const loadSystemFile = ( | |
| filePath: string, | |
| fileLabel: string, | |
| ): { settings: Settings; rawSettings: Settings; rawJson?: string } => { | |
| if (!fs.existsSync(filePath)) { | |
| return { settings: {}, rawSettings: {} }; | |
| } | |
| const check = isFileAndDirectorySecureSync(filePath, securityCache); | |
| if (!check.secure) { | |
| settingsErrors.push({ | |
| message: `Security Warning: Skipping ${fileLabel} file '${filePath}': ${check.reason}`, | |
| path: filePath, | |
| severity: 'warning', | |
| }); | |
| return { settings: {}, rawSettings: {} }; | |
| } | |
| return load(filePath); | |
| }; | |
| const systemResult = loadSystemFile(systemSettingsPath, 'system settings'); | |
| const systemDefaultsResult = loadSystemFile( | |
| systemDefaultsPath, | |
| 'system defaults', | |
| ); | |
| const userResult = load(USER_SETTINGS_PATH); | |
| let workspaceResult: { | |
| settings: Settings; | |
| rawSettings: Settings; | |
| rawJson?: string; | |
| } = { | |
| settings: {} as Settings, | |
| rawSettings: {} as Settings, | |
| rawJson: undefined, | |
| }; | |
| if (!storage.isWorkspaceHomeDir()) { | |
| workspaceResult = load(workspaceSettingsPath); | |
| } | |
| const systemOriginalSettings = structuredClone(systemResult.rawSettings); | |
| const systemDefaultsOriginalSettings = structuredClone( | |
| systemDefaultsResult.rawSettings, | |
| ); | |
| const userOriginalSettings = structuredClone(userResult.rawSettings); | |
| const workspaceOriginalSettings = structuredClone( | |
| workspaceResult.rawSettings, | |
| ); | |
| // Environment variables for runtime use are already resolved and validated in load() | |
| systemSettings = systemResult.settings; | |
| systemDefaultSettings = systemDefaultsResult.settings; | |
| userSettings = userResult.settings; | |
| workspaceSettings = 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, | |
| getDefaultsFromSchema(), | |
| systemDefaultSettings, | |
| userSettings, | |
| systemSettings, | |
| ); | |
| const isTrusted = | |
| isWorkspaceTrusted(initialTrustCheckSettings as Settings, workspaceDir) | |
| ?.isTrusted ?? false; | |
| // Create a temporary merged settings object to pass to loadEnvironment. | |
| const tempMergedSettings = mergeSettings( | |
| systemSettings, | |
| systemDefaultSettings, | |
| userSettings, | |
| workspaceSettings, | |
| isTrusted, | |
| ); | |
| // loadEnvironment depends on settings so we have to create a temp version of | |
| // the settings to avoid a cycle | |
| loadEnvironment(tempMergedSettings, workspaceDir); | |
| // Check for any fatal errors before proceeding | |
| const fatalErrors = settingsErrors.filter((e) => e.severity === 'error'); | |
| if (fatalErrors.length > 0) { | |
| const errorMessages = fatalErrors.map( | |
| (error) => `Error in ${error.path}: ${error.message}`, | |
| ); | |
| throw new FatalConfigError( | |
| `${errorMessages.join('\n')}\nPlease fix the configuration file(s) and try again.`, | |
| ); | |
| } | |
| const loadedSettings = new LoadedSettings( | |
| { | |
| path: systemSettingsPath, | |
| settings: systemSettings, | |
| originalSettings: systemOriginalSettings, | |
| rawJson: systemResult.rawJson, | |
| readOnly: true, | |
| }, | |
| { | |
| path: systemDefaultsPath, | |
| settings: systemDefaultSettings, | |
| originalSettings: systemDefaultsOriginalSettings, | |
| rawJson: systemDefaultsResult.rawJson, | |
| readOnly: true, | |
| }, | |
| { | |
| path: USER_SETTINGS_PATH, | |
| settings: userSettings, | |
| originalSettings: userOriginalSettings, | |
| rawJson: userResult.rawJson, | |
| readOnly: false, | |
| }, | |
| { | |
| path: storage.isWorkspaceHomeDir() ? '' : workspaceSettingsPath, | |
| settings: workspaceSettings, | |
| originalSettings: workspaceOriginalSettings, | |
| rawJson: workspaceResult.rawJson, | |
| readOnly: storage.isWorkspaceHomeDir(), | |
| }, | |
| isTrusted, | |
| settingsErrors, | |
| ); | |
| // Automatically migrate deprecated settings when loading. | |
| migrateDeprecatedSettings(loadedSettings); | |
| return loadedSettings; | |
| } | |
| /** | |
| * Migrates deprecated settings to their new counterparts. | |
| * | |
| * Deprecated settings are removed from settings files by default. | |
| * | |
| * @returns true if any changes were made and need to be saved. | |
| */ | |
| export function migrateDeprecatedSettings( | |
| loadedSettings: LoadedSettings, | |
| removeDeprecated = true, | |
| ): boolean { | |
| let anyModified = false; | |
| const systemWarnings: Map<LoadableSettingScope, string[]> = new Map(); | |
| /** | |
| * Helper to migrate a boolean setting and track it if it's deprecated. | |
| */ | |
| const migrateBoolean = ( | |
| settings: Record<string, unknown>, | |
| oldKey: string, | |
| newKey: string, | |
| prefix: string, | |
| foundDeprecated?: string[], | |
| ): boolean => { | |
| let modified = false; | |
| const oldValue = settings[oldKey]; | |
| const newValue = settings[newKey]; | |
| if (typeof oldValue === 'boolean') { | |
| if (foundDeprecated) { | |
| foundDeprecated.push(prefix ? `${prefix}.${oldKey}` : oldKey); | |
| } | |
| if (typeof newValue === 'boolean') { | |
| // Both exist, trust the new one | |
| if (removeDeprecated) { | |
| delete settings[oldKey]; | |
| modified = true; | |
| } | |
| } else { | |
| // Only old exists, migrate to new (inverted) | |
| settings[newKey] = !oldValue; | |
| if (removeDeprecated) { | |
| delete settings[oldKey]; | |
| } | |
| modified = true; | |
| } | |
| } | |
| return modified; | |
| }; | |
| const processScope = (scope: LoadableSettingScope) => { | |
| const settingsFile = loadedSettings.forScope(scope); | |
| const settings = settingsFile.settings; | |
| const foundDeprecated: string[] = []; | |
| // Migrate general settings | |
| const generalSettings = settings.general as | |
| | Record<string, unknown> | |
| | undefined; | |
| if (generalSettings) { | |
| const newGeneral = { ...generalSettings }; | |
| let modified = false; | |
| modified = | |
| migrateBoolean( | |
| newGeneral, | |
| 'disableAutoUpdate', | |
| 'enableAutoUpdate', | |
| 'general', | |
| foundDeprecated, | |
| ) || modified; | |
| modified = | |
| migrateBoolean( | |
| newGeneral, | |
| 'disableUpdateNag', | |
| 'enableAutoUpdateNotification', | |
| 'general', | |
| foundDeprecated, | |
| ) || modified; | |
| if (modified) { | |
| loadedSettings.setValue(scope, 'general', newGeneral); | |
| if (!settingsFile.readOnly) { | |
| anyModified = true; | |
| } | |
| } | |
| } | |
| // Migrate ui settings | |
| const uiSettings = settings.ui as Record<string, unknown> | undefined; | |
| if (uiSettings) { | |
| const newUi = { ...uiSettings }; | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| const accessibilitySettings = newUi['accessibility'] as | |
| | Record<string, unknown> | |
| | undefined; | |
| if (accessibilitySettings) { | |
| const newAccessibility = { ...accessibilitySettings }; | |
| if ( | |
| migrateBoolean( | |
| newAccessibility, | |
| 'disableLoadingPhrases', | |
| 'enableLoadingPhrases', | |
| 'ui.accessibility', | |
| foundDeprecated, | |
| ) | |
| ) { | |
| newUi['accessibility'] = newAccessibility; | |
| loadedSettings.setValue(scope, 'ui', newUi); | |
| if (!settingsFile.readOnly) { | |
| anyModified = true; | |
| } | |
| } | |
| // Migrate enableLoadingPhrases: false → loadingPhrases: 'off' | |
| const enableLP = newAccessibility['enableLoadingPhrases']; | |
| if ( | |
| typeof enableLP === 'boolean' && | |
| newUi['loadingPhrases'] === undefined | |
| ) { | |
| if (!enableLP) { | |
| newUi['loadingPhrases'] = 'off'; | |
| loadedSettings.setValue(scope, 'ui', newUi); | |
| if (!settingsFile.readOnly) { | |
| anyModified = true; | |
| } | |
| } | |
| foundDeprecated.push('ui.accessibility.enableLoadingPhrases'); | |
| } | |
| } | |
| } | |
| // Migrate context settings | |
| const contextSettings = settings.context as | |
| | Record<string, unknown> | |
| | undefined; | |
| if (contextSettings) { | |
| const newContext = { ...contextSettings }; | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| const fileFilteringSettings = newContext['fileFiltering'] as | |
| | Record<string, unknown> | |
| | undefined; | |
| if (fileFilteringSettings) { | |
| const newFileFiltering = { ...fileFilteringSettings }; | |
| if ( | |
| migrateBoolean( | |
| newFileFiltering, | |
| 'disableFuzzySearch', | |
| 'enableFuzzySearch', | |
| 'context.fileFiltering', | |
| foundDeprecated, | |
| ) | |
| ) { | |
| newContext['fileFiltering'] = newFileFiltering; | |
| loadedSettings.setValue(scope, 'context', newContext); | |
| if (!settingsFile.readOnly) { | |
| anyModified = true; | |
| } | |
| } | |
| } | |
| } | |
| // Migrate tools settings | |
| const toolsSettings = settings.tools as Record<string, unknown> | undefined; | |
| if (toolsSettings) { | |
| if (toolsSettings['approvalMode'] !== undefined) { | |
| foundDeprecated.push('tools.approvalMode'); | |
| const generalSettings = | |
| (settings.general as Record<string, unknown> | undefined) || {}; | |
| const newGeneral = { ...generalSettings }; | |
| // Only set defaultApprovalMode if it's not already set | |
| if (newGeneral['defaultApprovalMode'] === undefined) { | |
| newGeneral['defaultApprovalMode'] = toolsSettings['approvalMode']; | |
| loadedSettings.setValue(scope, 'general', newGeneral); | |
| if (!settingsFile.readOnly) { | |
| anyModified = true; | |
| } | |
| } | |
| if (removeDeprecated) { | |
| const newTools = { ...toolsSettings }; | |
| delete newTools['approvalMode']; | |
| loadedSettings.setValue(scope, 'tools', newTools); | |
| if (!settingsFile.readOnly) { | |
| anyModified = true; | |
| } | |
| } | |
| } | |
| } | |
| // Migrate experimental agent settings | |
| const experimentalModified = migrateExperimentalSettings( | |
| settings, | |
| loadedSettings, | |
| scope, | |
| removeDeprecated, | |
| foundDeprecated, | |
| ); | |
| if (experimentalModified) { | |
| if (!settingsFile.readOnly) { | |
| anyModified = true; | |
| } | |
| } | |
| if (settingsFile.readOnly && foundDeprecated.length > 0) { | |
| systemWarnings.set(scope, foundDeprecated); | |
| } | |
| }; | |
| processScope(SettingScope.User); | |
| processScope(SettingScope.Workspace); | |
| processScope(SettingScope.System); | |
| processScope(SettingScope.SystemDefaults); | |
| if (systemWarnings.size > 0) { | |
| for (const [scope, flags] of systemWarnings) { | |
| const scopeName = | |
| scope === SettingScope.SystemDefaults | |
| ? 'system default' | |
| : scope.toLowerCase(); | |
| coreEvents.emitFeedback( | |
| 'warning', | |
| `The ${scopeName} configuration contains deprecated settings: [${flags.join(', ')}]. These could not be migrated automatically as system settings are read-only. Please update the system configuration manually.`, | |
| ); | |
| } | |
| } | |
| return anyModified; | |
| } | |
| export function saveSettings(settingsFile: SettingsFile): void { | |
| // Clear the entire cache on any save. | |
| settingsCache.clear(); | |
| try { | |
| // Ensure the directory exists | |
| const dirPath = path.dirname(settingsFile.path); | |
| if (!fs.existsSync(dirPath)) { | |
| fs.mkdirSync(dirPath, { recursive: true }); | |
| } | |
| const settingsToSave = settingsFile.originalSettings; | |
| // Use the format-preserving update function | |
| updateSettingsFilePreservingFormat( | |
| settingsFile.path, | |
| settingsToSave as Record<string, unknown>, | |
| ); | |
| } catch (error) { | |
| const detailedErrorMessage = getFsErrorMessage(error); | |
| coreEvents.emitFeedback( | |
| 'error', | |
| `Failed to save settings: ${detailedErrorMessage}`, | |
| error, | |
| ); | |
| } | |
| } | |
| export function saveModelChange( | |
| loadedSettings: LoadedSettings, | |
| model: string, | |
| ): void { | |
| try { | |
| loadedSettings.setValue(SettingScope.User, 'model.name', model); | |
| } catch (error) { | |
| const detailedErrorMessage = getFsErrorMessage(error); | |
| coreEvents.emitFeedback( | |
| 'error', | |
| `Failed to save preferred model: ${detailedErrorMessage}`, | |
| error, | |
| ); | |
| } | |
| } | |
| function migrateExperimentalSettings( | |
| settings: Settings, | |
| loadedSettings: LoadedSettings, | |
| scope: LoadableSettingScope, | |
| removeDeprecated: boolean, | |
| foundDeprecated?: string[], | |
| ): boolean { | |
| const experimentalSettings = settings.experimental as | |
| | Record<string, unknown> | |
| | undefined; | |
| if (experimentalSettings) { | |
| const agentsSettings = { | |
| ...(settings.agents as Record<string, unknown> | undefined), | |
| }; | |
| const agentsOverrides = { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| ...((agentsSettings['overrides'] as Record<string, unknown>) || {}), | |
| }; | |
| let modified = false; | |
| const migrateExperimental = <T = Record<string, unknown>>( | |
| oldKey: string, | |
| migrateFn: (oldValue: T) => void, | |
| ) => { | |
| const old = experimentalSettings[oldKey]; | |
| if (old !== undefined) { | |
| foundDeprecated?.push(`experimental.${oldKey}`); | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| migrateFn(old as T); | |
| modified = true; | |
| } | |
| }; | |
| // Migrate codebaseInvestigatorSettings -> agents.overrides.codebase_investigator | |
| migrateExperimental('codebaseInvestigatorSettings', (old) => { | |
| const override = { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| ...(agentsOverrides['codebase_investigator'] as | |
| | Record<string, unknown> | |
| | undefined), | |
| }; | |
| if (old['enabled'] !== undefined) override['enabled'] = old['enabled']; | |
| const runConfig = { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| ...(override['runConfig'] as Record<string, unknown> | undefined), | |
| }; | |
| if (old['maxNumTurns'] !== undefined) | |
| runConfig['maxTurns'] = old['maxNumTurns']; | |
| if (old['maxTimeMinutes'] !== undefined) | |
| runConfig['maxTimeMinutes'] = old['maxTimeMinutes']; | |
| if (Object.keys(runConfig).length > 0) override['runConfig'] = runConfig; | |
| if (old['model'] !== undefined || old['thinkingBudget'] !== undefined) { | |
| const modelConfig = { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| ...(override['modelConfig'] as Record<string, unknown> | undefined), | |
| }; | |
| if (old['model'] !== undefined) modelConfig['model'] = old['model']; | |
| if (old['thinkingBudget'] !== undefined) { | |
| const generateContentConfig = { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| ...(modelConfig['generateContentConfig'] as | |
| | Record<string, unknown> | |
| | undefined), | |
| }; | |
| const thinkingConfig = { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| ...(generateContentConfig['thinkingConfig'] as | |
| | Record<string, unknown> | |
| | undefined), | |
| }; | |
| thinkingConfig['thinkingBudget'] = old['thinkingBudget']; | |
| generateContentConfig['thinkingConfig'] = thinkingConfig; | |
| modelConfig['generateContentConfig'] = generateContentConfig; | |
| } | |
| override['modelConfig'] = modelConfig; | |
| } | |
| agentsOverrides['codebase_investigator'] = override; | |
| }); | |
| // Migrate cliHelpAgentSettings -> agents.overrides.cli_help | |
| migrateExperimental('cliHelpAgentSettings', (old) => { | |
| const override = { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| ...(agentsOverrides['cli_help'] as Record<string, unknown> | undefined), | |
| }; | |
| if (old['enabled'] !== undefined) override['enabled'] = old['enabled']; | |
| agentsOverrides['cli_help'] = override; | |
| }); | |
| // Migrate experimental.plan -> general.plan.enabled | |
| migrateExperimental<boolean>('plan', (planValue) => { | |
| const generalSettings = | |
| (settings.general as Record<string, unknown> | undefined) || {}; | |
| const newGeneral = { ...generalSettings }; | |
| const planSettings = | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| (newGeneral['plan'] as Record<string, unknown> | undefined) || {}; | |
| const newPlan = { ...planSettings }; | |
| if (newPlan['enabled'] === undefined) { | |
| newPlan['enabled'] = planValue; | |
| newGeneral['plan'] = newPlan; | |
| loadedSettings.setValue(scope, 'general', newGeneral); | |
| modified = true; | |
| } | |
| }); | |
| if (modified) { | |
| agentsSettings['overrides'] = agentsOverrides; | |
| loadedSettings.setValue(scope, 'agents', agentsSettings); | |
| if (removeDeprecated) { | |
| const newExperimental = { ...experimentalSettings }; | |
| delete newExperimental['codebaseInvestigatorSettings']; | |
| delete newExperimental['cliHelpAgentSettings']; | |
| delete newExperimental['plan']; | |
| loadedSettings.setValue(scope, 'experimental', newExperimental); | |
| } | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |