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