release: Gemini CLI v0.34.0 (6 patches)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
<!-- VERSION_BADGE:START -->
|
||||
Patched Gemini CLI for use with custom API endpoints.
|
||||
Latest: **v0.33.2** (13 patches).
|
||||
Latest: **v0.34.0** (13 patches).
|
||||
<!-- VERSION_BADGE:END -->
|
||||
|
||||
## npm Install (Recommended)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"gemini-3.1-pro-low",
|
||||
"gemini-3.1-flash-image"
|
||||
],
|
||||
"target_version": "0.33.2",
|
||||
"target_version": "0.34.0",
|
||||
"telemetry_enabled": false,
|
||||
"npm_package": "@google/gemini-cli",
|
||||
"npm_registry": "https://npm.sensey24.ru"
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"latest": "0.33.2",
|
||||
"latest": "0.34.0",
|
||||
"releases": [
|
||||
{
|
||||
"version": "0.34.0",
|
||||
"date": "2026-03-18",
|
||||
"patches": 6,
|
||||
"status": "stable"
|
||||
},
|
||||
{
|
||||
"version": "0.33.2",
|
||||
"date": "2026-03-17",
|
||||
|
||||
17455
gemini/releases/v0.34.0/index.mjs
Normal file
17455
gemini/releases/v0.34.0/index.mjs
Normal file
File diff suppressed because it is too large
Load Diff
823
gemini/releases/v0.34.0/settings.js
Normal file
823
gemini/releases/v0.34.0/settings.js
Normal file
@@ -0,0 +1,823 @@
|
||||
/**
|
||||
* @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, Storage, coreEvents, homedir, createCache, } 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 { getSettingsSchema, } from './settingsSchema.js';
|
||||
export { 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) {
|
||||
let current = undefined;
|
||||
let currentSchema = getSettingsSchema();
|
||||
let parent = 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'];
|
||||
const AUTH_ENV_VAR_WHITELIST = [
|
||||
'GEMINI_API_KEY',
|
||||
'GOOGLE_API_KEY',
|
||||
'GOOGLE_CLOUD_PROJECT',
|
||||
'GOOGLE_CLOUD_LOCATION',
|
||||
'GOOGLE_GEMINI_BASE_URL',
|
||||
'GOOGLE_VERTEX_BASE_URL',
|
||||
];
|
||||
/**
|
||||
* Sanitizes an environment variable value to prevent shell injection.
|
||||
* Restricts values to a safe character set: alphanumeric, -, _, ., /
|
||||
*/
|
||||
export function sanitizeEnvVar(value) {
|
||||
return value.replace(/[^a-zA-Z0-9\-_./:@]/g, '');
|
||||
}
|
||||
export function getSystemSettingsPath() {
|
||||
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() {
|
||||
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 var SettingScope;
|
||||
(function (SettingScope) {
|
||||
SettingScope["User"] = "User";
|
||||
SettingScope["Workspace"] = "Workspace";
|
||||
SettingScope["System"] = "System";
|
||||
SettingScope["SystemDefaults"] = "SystemDefaults";
|
||||
// Note that this scope is not supported in the settings dialog at this time,
|
||||
// it is only supported for extensions.
|
||||
SettingScope["Session"] = "Session";
|
||||
})(SettingScope || (SettingScope = {}));
|
||||
/**
|
||||
* 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) {
|
||||
return _loadableSettingScopes.includes(scope);
|
||||
}
|
||||
function setNestedProperty(obj, path, value) {
|
||||
const keys = path.split('.');
|
||||
const lastKey = keys.pop();
|
||||
if (!lastKey)
|
||||
return;
|
||||
let current = 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;
|
||||
}
|
||||
else {
|
||||
// This path is invalid, so we stop.
|
||||
return;
|
||||
}
|
||||
}
|
||||
current[lastKey] = value;
|
||||
}
|
||||
export function getDefaultsFromSchema(schema = getSettingsSchema()) {
|
||||
const defaults = {};
|
||||
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;
|
||||
}
|
||||
export function mergeSettings(system, systemDefaults, user, workspace, isTrusted) {
|
||||
const safeWorkspace = isTrusted ? workspace : {};
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* 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 = {}) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return customDeepMerge(getMergeStrategyForPath, getDefaultsFromSchema(), overrides);
|
||||
}
|
||||
export class LoadedSettings {
|
||||
constructor(system, systemDefaults, user, workspace, isTrusted, errors = []) {
|
||||
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();
|
||||
}
|
||||
system;
|
||||
systemDefaults;
|
||||
user;
|
||||
workspace;
|
||||
isTrusted;
|
||||
errors;
|
||||
_workspaceFile;
|
||||
_merged;
|
||||
_snapshot;
|
||||
_remoteAdminSettings;
|
||||
get merged() {
|
||||
return this._merged;
|
||||
}
|
||||
setTrusted(isTrusted) {
|
||||
if (this.isTrusted === isTrusted) {
|
||||
return;
|
||||
}
|
||||
this.isTrusted = isTrusted;
|
||||
this.workspace = isTrusted
|
||||
? this._workspaceFile
|
||||
: this.createEmptyWorkspace(this._workspaceFile);
|
||||
this._merged = this.computeMergedSettings();
|
||||
coreEvents.emitSettingsChanged();
|
||||
}
|
||||
createEmptyWorkspace(workspace) {
|
||||
return {
|
||||
...workspace,
|
||||
settings: {},
|
||||
originalSettings: {},
|
||||
};
|
||||
}
|
||||
computeMergedSettings() {
|
||||
const merged = mergeSettings(this.system.settings, this.systemDefaults.settings, this.user.settings, this.workspace.settings, this.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) => getMergeStrategyForPath(['admin', ...path]), adminDefaults, this._remoteAdminSettings?.admin ?? {});
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
computeSnapshot() {
|
||||
const cloneSettingsFile = (file) => ({
|
||||
path: file.path,
|
||||
rawJson: file.rawJson,
|
||||
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) {
|
||||
coreEvents.on(CoreEvent.SettingsChanged, listener);
|
||||
return () => coreEvents.off(CoreEvent.SettingsChanged, listener);
|
||||
}
|
||||
getSnapshot() {
|
||||
return this._snapshot;
|
||||
}
|
||||
forScope(scope) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
isPersistable(settingsFile) {
|
||||
return !settingsFile.readOnly;
|
||||
}
|
||||
setValue(scope, key, value) {
|
||||
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) {
|
||||
const 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,
|
||||
};
|
||||
admin.extensions = {
|
||||
enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled,
|
||||
};
|
||||
admin.skills = {
|
||||
enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled,
|
||||
};
|
||||
this._remoteAdminSettings = { admin };
|
||||
this._merged = this.computeMergedSettings();
|
||||
}
|
||||
}
|
||||
function findEnvFile(startDir) {
|
||||
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, isTrusted, isSandboxed) {
|
||||
// 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.
|
||||
let value = 'cloudshell-gca';
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] = value;
|
||||
}
|
||||
export function loadEnvironment(settings, workspaceDir, isWorkspaceTrustedFn = isWorkspaceTrusted) {
|
||||
const envFilePath = findEnvFile(workspaceDir);
|
||||
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');
|
||||
// Cloud Shell environment variable handling
|
||||
if (process.env['CLOUD_SHELL'] === 'true') {
|
||||
setUpCloudShellEnvironment(envFilePath, isTrusted, isSandboxed);
|
||||
}
|
||||
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 but we are sandboxed, only allow whitelisted variables.
|
||||
if (!isTrusted && isSandboxed) {
|
||||
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 (_e) {
|
||||
// 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({
|
||||
storage: 'map',
|
||||
defaultTtl: 10000, // 10 seconds
|
||||
});
|
||||
/**
|
||||
* Resets the settings cache. Used exclusively for test isolation.
|
||||
* @internal
|
||||
*/
|
||||
export function resetSettingsCacheForTesting() {
|
||||
settingsCache.clear();
|
||||
}
|
||||
/**
|
||||
* Loads settings from user and workspace directories.
|
||||
* Project settings override user settings.
|
||||
*/
|
||||
export function loadSettings(workspaceDir = process.cwd()) {
|
||||
const normalizedWorkspaceDir = path.resolve(workspaceDir);
|
||||
return settingsCache.getOrCreate(normalizedWorkspaceDir, () => _doLoadSettings(normalizedWorkspaceDir));
|
||||
}
|
||||
/**
|
||||
* Internal implementation of the settings loading logic.
|
||||
*/
|
||||
function _doLoadSettings(workspaceDir) {
|
||||
let systemSettings = {};
|
||||
let systemDefaultSettings = {};
|
||||
let userSettings = {};
|
||||
let workspaceSettings = {};
|
||||
const settingsErrors = [];
|
||||
const systemSettingsPath = getSystemSettingsPath();
|
||||
const systemDefaultsPath = getSystemDefaultsPath();
|
||||
const storage = new Storage(workspaceDir);
|
||||
const workspaceSettingsPath = storage.getWorkspaceSettingsPath();
|
||||
const load = (filePath) => {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const rawSettings = 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: {} };
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const settingsObject = rawSettings;
|
||||
// Validate settings structure with Zod
|
||||
const validationResult = validateSettings(settingsObject);
|
||||
if (!validationResult.success && validationResult.error) {
|
||||
const errorMessage = formatValidationError(validationResult.error, filePath);
|
||||
settingsErrors.push({
|
||||
message: errorMessage,
|
||||
path: filePath,
|
||||
severity: 'warning',
|
||||
});
|
||||
}
|
||||
return { settings: settingsObject, rawJson: content };
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
settingsErrors.push({
|
||||
message: getErrorMessage(error),
|
||||
path: filePath,
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
return { settings: {} };
|
||||
};
|
||||
const systemResult = load(systemSettingsPath);
|
||||
const systemDefaultsResult = load(systemDefaultsPath);
|
||||
const userResult = load(USER_SETTINGS_PATH);
|
||||
let workspaceResult = {
|
||||
settings: {},
|
||||
rawJson: undefined,
|
||||
};
|
||||
if (!storage.isWorkspaceHomeDir()) {
|
||||
workspaceResult = load(workspaceSettingsPath);
|
||||
}
|
||||
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, getDefaultsFromSchema(), systemDefaultSettings, userSettings, systemSettings);
|
||||
const isTrusted = isWorkspaceTrusted(initialTrustCheckSettings, 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, removeDeprecated = true) {
|
||||
let anyModified = false;
|
||||
const systemWarnings = new Map();
|
||||
/**
|
||||
* Helper to migrate a boolean setting and track it if it's deprecated.
|
||||
*/
|
||||
const migrateBoolean = (settings, oldKey, newKey, prefix, foundDeprecated) => {
|
||||
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) => {
|
||||
const settingsFile = loadedSettings.forScope(scope);
|
||||
const settings = settingsFile.settings;
|
||||
const foundDeprecated = [];
|
||||
// Migrate general settings
|
||||
const generalSettings = settings.general;
|
||||
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;
|
||||
if (uiSettings) {
|
||||
const newUi = { ...uiSettings };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const accessibilitySettings = newUi['accessibility'];
|
||||
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;
|
||||
if (contextSettings) {
|
||||
const newContext = { ...contextSettings };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const fileFilteringSettings = newContext['fileFiltering'];
|
||||
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;
|
||||
if (toolsSettings) {
|
||||
if (toolsSettings['approvalMode'] !== undefined) {
|
||||
foundDeprecated.push('tools.approvalMode');
|
||||
const generalSettings = settings.general || {};
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
catch (error) {
|
||||
coreEvents.emitFeedback('error', 'There was an error saving your latest settings changes.', error);
|
||||
}
|
||||
}
|
||||
export function saveModelChange(loadedSettings, model) {
|
||||
try {
|
||||
loadedSettings.setValue(SettingScope.User, 'model.name', model);
|
||||
}
|
||||
catch (error) {
|
||||
coreEvents.emitFeedback('error', 'There was an error saving your preferred model.', error);
|
||||
}
|
||||
}
|
||||
function migrateExperimentalSettings(settings, loadedSettings, scope, removeDeprecated, foundDeprecated) {
|
||||
const experimentalSettings = settings.experimental;
|
||||
if (experimentalSettings) {
|
||||
const agentsSettings = {
|
||||
...settings.agents,
|
||||
};
|
||||
const agentsOverrides = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
...(agentsSettings['overrides'] || {}),
|
||||
};
|
||||
let modified = false;
|
||||
const migrateExperimental = (oldKey, migrateFn) => {
|
||||
const old = experimentalSettings[oldKey];
|
||||
if (old) {
|
||||
foundDeprecated?.push(`experimental.${oldKey}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
migrateFn(old);
|
||||
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'],
|
||||
};
|
||||
if (old['enabled'] !== undefined)
|
||||
override['enabled'] = old['enabled'];
|
||||
const runConfig = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
...override['runConfig'],
|
||||
};
|
||||
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'],
|
||||
};
|
||||
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'],
|
||||
};
|
||||
const thinkingConfig = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
...generateContentConfig['thinkingConfig'],
|
||||
};
|
||||
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'],
|
||||
};
|
||||
if (old['enabled'] !== undefined)
|
||||
override['enabled'] = old['enabled'];
|
||||
agentsOverrides['cli_help'] = override;
|
||||
});
|
||||
if (modified) {
|
||||
agentsSettings['overrides'] = agentsOverrides;
|
||||
loadedSettings.setValue(scope, 'agents', agentsSettings);
|
||||
if (removeDeprecated) {
|
||||
const newExperimental = { ...experimentalSettings };
|
||||
delete newExperimental['codebaseInvestigatorSettings'];
|
||||
delete newExperimental['cliHelpAgentSettings'];
|
||||
loadedSettings.setValue(scope, 'experimental', newExperimental);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//# sourceMappingURL=settings.js.map
|
||||
Reference in New Issue
Block a user