Files
gemini-cli/packages/cli/src/config/settings.ts

877 lines
27 KiB
TypeScript

/**
* @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 {
debugLogger,
FatalConfigError,
GEMINI_DIR,
getErrorMessage,
Storage,
coreEvents,
} 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 {
type Settings,
type MemoryImportFormat,
type MergeStrategy,
type SettingsSchema,
type SettingDefinition,
getSettingsSchema,
} from './settingsSchema.js';
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
import { customDeepMerge, type MergeableObject } from '../utils/deepMerge.js';
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
import type { ExtensionManager } from './extension-manager.js';
import {
validateSettings,
formatValidationError,
} from './settings-validation.js';
import { SettingPaths } from './settingPaths.js';
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 type { Settings, MemoryImportFormat };
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: Record<string, string> = {
accessibility: 'ui.accessibility',
allowedTools: 'tools.allowed',
allowMCPServers: 'mcp.allowed',
autoAccept: 'tools.autoAccept',
autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory',
bugCommand: 'advanced.bugCommand',
chatCompression: 'model.compressionThreshold',
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',
enableHooks: 'tools.enableHooks',
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',
showStatusInTitle: 'ui.showStatusInTitle',
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: SettingPaths.General.PreferredEditor,
retryFetchErrors: 'general.retryFetchErrors',
sandbox: 'tools.sandbox',
selectedAuthType: 'security.auth.selectedType',
enableInteractiveShell: 'tools.shell.enableInteractiveShell',
shellPager: 'tools.shell.pager',
shellShowColor: 'tools.shell.showColor',
shellInactivityTimeout: 'tools.shell.inactivityTimeout',
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(): 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 interface AccessibilitySettings {
disableLoadingPhrases?: 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;
}
export interface SettingsFile {
settings: Settings;
originalSettings: Settings;
path: string;
rawJson?: string;
}
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) {
current = next as Record<string, unknown>;
} else {
// This path is invalid, so we stop.
return;
}
}
current[lastKey] = value;
}
export function needsMigration(settings: Record<string, unknown>): boolean {
// 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 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: Record<string, unknown>,
): Record<string, unknown> | null {
if (!needsMigration(flatSettings)) {
return null;
}
const v2Settings: Record<string, unknown> = {};
const flatKeys = new Set(Object.keys(flatSettings));
for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) {
if (flatKeys.has(oldKey)) {
// If the key exists and is a V2 container (like 'model'), and the value is an object,
// it is likely already migrated or partially migrated. We should not move it
// to the mapped V2 path (e.g. 'model' -> 'model.name').
// Instead, let it fall through to the "Carry over" section to be merged.
if (
KNOWN_V2_CONTAINERS.has(oldKey) &&
typeof flatSettings[oldKey] === 'object' &&
flatSettings[oldKey] !== null &&
!Array.isArray(flatSettings[oldKey])
) {
continue;
}
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: string[]) =>
getMergeStrategyForPath([remainingKey, ...path]);
v2Settings[remainingKey] = customDeepMerge(
pathAwareGetStrategy,
{},
existingValue as MergeableObject,
newValue as MergeableObject,
);
} else {
v2Settings[remainingKey] = newValue;
}
}
return v2Settings;
}
function getNestedProperty(
obj: Record<string, unknown>,
path: string,
): unknown {
const keys = path.split('.');
let current: unknown = obj;
for (const key of keys) {
if (typeof current !== 'object' || current === null || !(key in current)) {
return undefined;
}
current = (current as Record<string, unknown>)[key];
}
return current;
}
const REVERSE_MIGRATION_MAP: Record<string, string> = 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: Record<string, unknown>,
): Record<string, unknown> {
const v1Settings: Record<string, unknown> = {};
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: Settings,
systemDefaults: Settings,
user: Settings,
workspace: Settings,
isTrusted: boolean,
): Settings {
const safeWorkspace = isTrusted ? workspace : ({} as Settings);
// 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,
) as Settings;
}
export class LoadedSettings {
constructor(
system: SettingsFile,
systemDefaults: SettingsFile,
user: SettingsFile,
workspace: SettingsFile,
isTrusted: boolean,
migratedInMemoryScopes: Set<SettingScope>,
) {
this.system = system;
this.systemDefaults = systemDefaults;
this.user = user;
this.workspace = workspace;
this.isTrusted = isTrusted;
this.migratedInMemoryScopes = migratedInMemoryScopes;
this._merged = this.computeMergedSettings();
}
readonly system: SettingsFile;
readonly systemDefaults: SettingsFile;
readonly user: SettingsFile;
readonly workspace: SettingsFile;
readonly isTrusted: boolean;
readonly migratedInMemoryScopes: Set<SettingScope>;
private _merged: Settings;
get merged(): Settings {
return this._merged;
}
private computeMergedSettings(): Settings {
return mergeSettings(
this.system.settings,
this.systemDefaults.settings,
this.user.settings,
this.workspace.settings,
this.isTrusted,
);
}
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}`);
}
}
setValue(scope: LoadableSettingScope, key: string, value: unknown): void {
const settingsFile = this.forScope(scope);
setNestedProperty(settingsFile.settings, key, value);
setNestedProperty(settingsFile.originalSettings, key, value);
this._merged = this.computeMergedSettings();
saveSettings(settingsFile);
}
}
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;
}
}
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);
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: Settings): void {
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: string = process.cwd(),
): LoadedSettings {
let systemSettings: Settings = {};
let systemDefaultSettings: Settings = {};
let userSettings: Settings = {};
let workspaceSettings: Settings = {};
const settingsErrors: SettingsError[] = [];
const systemSettingsPath = getSystemSettingsPath();
const systemDefaultsPath = getSystemDefaultsPath();
const migratedInMemoryScopes = new Set<SettingScope>();
// 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: string,
scope: SettingScope,
): { settings: 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,
});
return { settings: {} };
}
let settingsObject = rawSettings as Record<string, unknown>;
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) {
coreEvents.emitFeedback(
'error',
'Failed to migrate settings file.',
e,
);
}
} else {
migratedInMemoryScopes.add(scope);
}
settingsObject = migratedSettings;
}
}
// Validate settings structure with Zod after migration
const validationResult = validateSettings(settingsObject);
if (!validationResult.success && validationResult.error) {
const errorMessage = formatValidationError(
validationResult.error,
filePath,
);
throw new FatalConfigError(errorMessage);
}
return { settings: settingsObject as Settings, rawJson: content };
}
} catch (error: unknown) {
// Preserve FatalConfigError with formatted validation messages
if (error instanceof FatalConfigError) {
throw 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: Settings; rawJson?: string } = {
settings: {} as 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 as Settings).isTrusted ?? true;
// 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);
// 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,
migratedInMemoryScopes,
);
}
export function migrateDeprecatedSettings(
loadedSettings: LoadedSettings,
extensionManager: ExtensionManager,
): void {
const processScope = (scope: LoadableSettingScope) => {
const settings = loadedSettings.forScope(scope).settings;
if (settings.extensions?.disabled) {
debugLogger.log(
`Migrating deprecated extensions.disabled settings from ${scope} settings...`,
);
for (const extension of settings.extensions.disabled ?? []) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
extensionManager.disableExtension(extension, scope);
}
const newExtensionsValue = { ...settings.extensions };
newExtensionsValue.disabled = undefined;
loadedSettings.setValue(scope, 'extensions', newExtensionsValue);
}
};
processScope(SettingScope.User);
processScope(SettingScope.Workspace);
}
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 });
}
let settingsToSave = settingsFile.originalSettings;
if (!MIGRATE_V2_OVERWRITE) {
settingsToSave = migrateSettingsToV1(
settingsToSave as Record<string, unknown>,
) as Settings;
}
// Use the format-preserving update function
updateSettingsFilePreservingFormat(
settingsFile.path,
settingsToSave as Record<string, unknown>,
);
} catch (error) {
coreEvents.emitFeedback(
'error',
'There was an error saving your latest settings changes.',
error,
);
}
}