diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index 228ace41..1ff178b8 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -24,7 +24,7 @@ Gemini CLI uses `settings.json` files for persistent configuration. There are th - **Location:** `.gemini/settings.json` within your project's root directory. - **Scope:** Applies only when running Gemini CLI from that specific project. Project settings override user settings. - **System settings file:** - - **Location:** `/etc/gemini-cli/settings.json` (Linux), `C:\ProgramData\gemini-cli\settings.json` (Windows) or `/Library/Application Support/GeminiCli/settings.json` (macOS). + - **Location:** `/etc/gemini-cli/settings.json` (Linux), `C:\ProgramData\gemini-cli\settings.json` (Windows) or `/Library/Application Support/GeminiCli/settings.json` (macOS). The path can be overridden using the `GEMINI_CLI_SYSTEM_SETTINGS_PATH` environment variable. - **Scope:** Applies to all Gemini CLI sessions on the system, for all users. System settings override user and project settings. May be useful for system administrators at enterprises to have controls over users' Gemini CLI setups. **Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`. diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index b99e8b79..ae655fe1 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -46,7 +46,7 @@ import stripJsonComments from 'strip-json-comments'; // Will be mocked separatel import { loadSettings, USER_SETTINGS_PATH, // This IS the mocked path. - SYSTEM_SETTINGS_PATH, + getSystemSettingsPath, SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock. SettingScope, } from './settings.js'; @@ -104,7 +104,7 @@ describe('Settings Loading and Merging', () => { it('should load system settings if only system file exists', () => { (mockFsExistsSync as Mock).mockImplementation( - (p: fs.PathLike) => p === SYSTEM_SETTINGS_PATH, + (p: fs.PathLike) => p === getSystemSettingsPath(), ); const systemSettingsContent = { theme: 'system-default', @@ -112,7 +112,7 @@ describe('Settings Loading and Merging', () => { }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { - if (p === SYSTEM_SETTINGS_PATH) + if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); return '{}'; }, @@ -121,7 +121,7 @@ describe('Settings Loading and Merging', () => { const settings = loadSettings(MOCK_WORKSPACE_DIR); expect(fs.readFileSync).toHaveBeenCalledWith( - SYSTEM_SETTINGS_PATH, + getSystemSettingsPath(), 'utf-8', ); expect(settings.system.settings).toEqual(systemSettingsContent); @@ -257,7 +257,7 @@ describe('Settings Loading and Merging', () => { (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { - if (p === SYSTEM_SETTINGS_PATH) + if (p === getSystemSettingsPath()) return JSON.stringify(systemSettingsContent); if (p === USER_SETTINGS_PATH) return JSON.stringify(userSettingsContent); @@ -743,7 +743,7 @@ describe('Settings Loading and Merging', () => { (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { - if (p === SYSTEM_SETTINGS_PATH) { + if (p === getSystemSettingsPath()) { process.env.SHARED_VAR = 'system_value_for_system_read'; // Set for system settings read return JSON.stringify(systemSettingsContent); } @@ -913,6 +913,50 @@ describe('Settings Loading and Merging', () => { delete process.env.TEST_HOST; delete process.env.TEST_PORT; }); + + describe('when GEMINI_CLI_SYSTEM_SETTINGS_PATH is set', () => { + const MOCK_ENV_SYSTEM_SETTINGS_PATH = '/mock/env/system/settings.json'; + + beforeEach(() => { + process.env.GEMINI_CLI_SYSTEM_SETTINGS_PATH = + MOCK_ENV_SYSTEM_SETTINGS_PATH; + }); + + afterEach(() => { + delete process.env.GEMINI_CLI_SYSTEM_SETTINGS_PATH; + }); + + it('should load system settings from the path specified in the environment variable', () => { + (mockFsExistsSync as Mock).mockImplementation( + (p: fs.PathLike) => p === MOCK_ENV_SYSTEM_SETTINGS_PATH, + ); + const systemSettingsContent = { + theme: 'env-var-theme', + sandbox: true, + }; + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === MOCK_ENV_SYSTEM_SETTINGS_PATH) + return JSON.stringify(systemSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + expect(fs.readFileSync).toHaveBeenCalledWith( + MOCK_ENV_SYSTEM_SETTINGS_PATH, + 'utf-8', + ); + expect(settings.system.path).toBe(MOCK_ENV_SYSTEM_SETTINGS_PATH); + expect(settings.system.settings).toEqual(systemSettingsContent); + expect(settings.merged).toEqual({ + ...systemSettingsContent, + customThemes: {}, + mcpServers: {}, + }); + }); + }); }); describe('LoadedSettings class', () => { diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 3cbfe22d..bc2206a7 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -25,7 +25,10 @@ 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'); -function getSystemSettingsPath(): string { +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') { @@ -35,8 +38,6 @@ function getSystemSettingsPath(): string { } } -export const SYSTEM_SETTINGS_PATH = getSystemSettingsPath(); - export enum SettingScope { User = 'User', Workspace = 'Workspace', @@ -297,11 +298,11 @@ export function loadSettings(workspaceDir: string): LoadedSettings { let userSettings: Settings = {}; let workspaceSettings: Settings = {}; const settingsErrors: SettingsError[] = []; - + const systemSettingsPath = getSystemSettingsPath(); // Load system settings try { - if (fs.existsSync(SYSTEM_SETTINGS_PATH)) { - const systemContent = fs.readFileSync(SYSTEM_SETTINGS_PATH, 'utf-8'); + if (fs.existsSync(systemSettingsPath)) { + const systemContent = fs.readFileSync(systemSettingsPath, 'utf-8'); const parsedSystemSettings = JSON.parse( stripJsonComments(systemContent), ) as Settings; @@ -310,7 +311,7 @@ export function loadSettings(workspaceDir: string): LoadedSettings { } catch (error: unknown) { settingsErrors.push({ message: getErrorMessage(error), - path: SYSTEM_SETTINGS_PATH, + path: systemSettingsPath, }); } @@ -368,7 +369,7 @@ export function loadSettings(workspaceDir: string): LoadedSettings { return new LoadedSettings( { - path: SYSTEM_SETTINGS_PATH, + path: systemSettingsPath, settings: systemSettings, }, {