mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 05:42:54 -07:00
foo
foo checkpoint
This commit is contained in:
@@ -327,11 +327,11 @@ export async function parseArguments(
|
||||
return true;
|
||||
});
|
||||
|
||||
if (settings.experimental?.extensionManagement) {
|
||||
if (settings.experimental.extensionManagement) {
|
||||
yargsInstance.command(extensionsCommand);
|
||||
}
|
||||
|
||||
if (settings.skills?.enabled ?? true) {
|
||||
if (settings.skills.enabled) {
|
||||
yargsInstance.command(skillsCommand);
|
||||
}
|
||||
// Register hooks command if hooks are enabled
|
||||
@@ -456,16 +456,16 @@ export async function loadCliConfig(
|
||||
process.env['GEMINI_SANDBOX'] = 'true';
|
||||
}
|
||||
|
||||
const memoryImportFormat = settings.context?.importFormat || 'tree';
|
||||
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
|
||||
const memoryImportFormat = settings.context.importFormat || 'tree';
|
||||
const includeDirectoryTree = settings.context.includeDirectoryTree;
|
||||
|
||||
const ideMode = settings.ide?.enabled ?? false;
|
||||
const ideMode = settings.ide.enabled;
|
||||
|
||||
const folderTrust =
|
||||
process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true' ||
|
||||
process.env['VITEST'] === 'true'
|
||||
? false
|
||||
: (settings.security?.folderTrust?.enabled ?? false);
|
||||
: settings.security.folderTrust.enabled;
|
||||
const trustedFolder =
|
||||
isWorkspaceTrusted(settings, cwd, undefined, {
|
||||
prompt: argv.prompt,
|
||||
@@ -476,7 +476,7 @@ export async function loadCliConfig(
|
||||
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
|
||||
// directly to the Config constructor in core, and have core handle setGeminiMdFilename.
|
||||
// However, loadHierarchicalGeminiMemory is called *before* createServerConfig.
|
||||
if (settings.context?.fileName) {
|
||||
if (settings.context.fileName) {
|
||||
setServerGeminiMdFilename(settings.context.fileName);
|
||||
} else {
|
||||
// Reset to default if not provided in settings.
|
||||
@@ -487,15 +487,15 @@ export async function loadCliConfig(
|
||||
|
||||
const memoryFileFiltering = {
|
||||
...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
|
||||
...settings.context?.fileFiltering,
|
||||
...settings.context.fileFiltering,
|
||||
};
|
||||
|
||||
const fileFiltering = {
|
||||
...DEFAULT_FILE_FILTERING_OPTIONS,
|
||||
...settings.context?.fileFiltering,
|
||||
...settings.context.fileFiltering,
|
||||
};
|
||||
|
||||
const includeDirectories = (settings.context?.includeDirectories || [])
|
||||
const includeDirectories = settings.context.includeDirectories
|
||||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
@@ -515,7 +515,7 @@ export async function loadCliConfig(
|
||||
.getExtensions()
|
||||
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
|
||||
|
||||
const experimentalJitContext = settings.experimental?.jitContext ?? false;
|
||||
const experimentalJitContext = settings.experimental.jitContext;
|
||||
|
||||
let memoryContent: string | HierarchicalMemory = '';
|
||||
let fileCount = 0;
|
||||
@@ -525,7 +525,7 @@ export async function loadCliConfig(
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
settings.context?.loadMemoryFromIncludeDirectories || false
|
||||
settings.context.loadMemoryFromIncludeDirectories
|
||||
? includeDirectories
|
||||
: [],
|
||||
debugMode,
|
||||
@@ -534,7 +534,7 @@ export async function loadCliConfig(
|
||||
trustedFolder,
|
||||
memoryImportFormat,
|
||||
memoryFileFiltering,
|
||||
settings.context?.discoveryMaxDirs,
|
||||
settings.context.discoveryMaxDirs,
|
||||
);
|
||||
memoryContent = result.memoryContent;
|
||||
fileCount = result.fileCount;
|
||||
@@ -548,8 +548,8 @@ export async function loadCliConfig(
|
||||
const rawApprovalMode =
|
||||
argv.approvalMode ||
|
||||
(argv.yolo ? 'yolo' : undefined) ||
|
||||
((settings.general?.defaultApprovalMode as string) !== 'yolo'
|
||||
? settings.general?.defaultApprovalMode
|
||||
((settings.general.defaultApprovalMode as string) !== 'yolo'
|
||||
? settings.general.defaultApprovalMode
|
||||
: undefined);
|
||||
|
||||
if (rawApprovalMode) {
|
||||
@@ -561,7 +561,7 @@ export async function loadCliConfig(
|
||||
approvalMode = ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case 'plan':
|
||||
if (!(settings.experimental?.plan ?? false)) {
|
||||
if (!settings.experimental.plan) {
|
||||
debugLogger.warn(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
|
||||
);
|
||||
@@ -583,9 +583,9 @@ export async function loadCliConfig(
|
||||
}
|
||||
|
||||
// Override approval mode if disableYoloMode is set.
|
||||
if (settings.security?.disableYoloMode || settings.admin?.secureModeEnabled) {
|
||||
if (settings.security.disableYoloMode || settings.admin.secureModeEnabled) {
|
||||
if (approvalMode === ApprovalMode.YOLO) {
|
||||
if (settings.admin?.secureModeEnabled) {
|
||||
if (settings.admin.secureModeEnabled) {
|
||||
debugLogger.error(
|
||||
'YOLO mode is disabled by "secureModeEnabled" setting.',
|
||||
);
|
||||
@@ -636,7 +636,7 @@ export async function loadCliConfig(
|
||||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
|
||||
!argv.isCommand);
|
||||
|
||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||
const allowedTools = argv.allowedTools || settings.tools.allowed || [];
|
||||
const allowedToolsSet = new Set(allowedTools);
|
||||
|
||||
// In non-interactive mode, exclude tools that require a prompt.
|
||||
@@ -694,7 +694,7 @@ export async function loadCliConfig(
|
||||
},
|
||||
mcp: {
|
||||
...settings.mcp,
|
||||
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
|
||||
allowed: argv.allowedMcpServerNames ?? settings.mcp.allowed,
|
||||
},
|
||||
policyPaths: argv.policy,
|
||||
};
|
||||
@@ -715,7 +715,7 @@ export async function loadCliConfig(
|
||||
|
||||
const defaultModel = PREVIEW_GEMINI_MODEL_AUTO;
|
||||
const specifiedModel =
|
||||
argv.model || process.env['GEMINI_MODEL'] || settings.model?.name;
|
||||
argv.model || process.env['GEMINI_MODEL'] || settings.model.name;
|
||||
|
||||
const resolvedModel =
|
||||
specifiedModel === GEMINI_MODEL_ALIAS_AUTO
|
||||
@@ -729,9 +729,9 @@ export async function loadCliConfig(
|
||||
|
||||
const ptyInfo = await getPty();
|
||||
|
||||
const mcpEnabled = settings.admin?.mcp?.enabled ?? true;
|
||||
const extensionsEnabled = settings.admin?.extensions?.enabled ?? true;
|
||||
const adminSkillsEnabled = settings.admin?.skills?.enabled ?? true;
|
||||
const mcpEnabled = settings.admin.mcp.enabled;
|
||||
const extensionsEnabled = settings.admin.extensions.enabled;
|
||||
const adminSkillsEnabled = settings.admin.skills.enabled;
|
||||
|
||||
// Create MCP enablement manager and callbacks
|
||||
const mcpEnablementManager = McpServerEnablementManager.getInstance();
|
||||
@@ -739,8 +739,8 @@ export async function loadCliConfig(
|
||||
? mcpEnablementManager.getEnablementCallbacks()
|
||||
: undefined;
|
||||
|
||||
const adminAllowlist = settings.admin?.mcp?.config;
|
||||
let mcpServerCommand = mcpEnabled ? settings.mcp?.serverCommand : undefined;
|
||||
const adminAllowlist = settings.admin.mcp.config;
|
||||
let mcpServerCommand = mcpEnabled ? settings.mcp.serverCommand : undefined;
|
||||
let mcpServers = mcpEnabled ? settings.mcpServers : {};
|
||||
|
||||
if (mcpEnabled && adminAllowlist && Object.keys(adminAllowlist).length > 0) {
|
||||
@@ -748,7 +748,7 @@ export async function loadCliConfig(
|
||||
mcpServers = result.mcpServers;
|
||||
mcpServerCommand = undefined;
|
||||
|
||||
if (result.blockedServerNames && result.blockedServerNames.length > 0) {
|
||||
if (result.blockedServerNames.length > 0) {
|
||||
const message = getAdminBlockedMcpServersMessage(
|
||||
result.blockedServerNames,
|
||||
undefined,
|
||||
@@ -766,17 +766,17 @@ export async function loadCliConfig(
|
||||
includeDirectoryTree,
|
||||
includeDirectories,
|
||||
loadMemoryFromIncludeDirectories:
|
||||
settings.context?.loadMemoryFromIncludeDirectories || false,
|
||||
settings.context.loadMemoryFromIncludeDirectories,
|
||||
debugMode,
|
||||
question,
|
||||
|
||||
coreTools: settings.tools?.core || undefined,
|
||||
coreTools: settings.tools.core || undefined,
|
||||
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
|
||||
policyEngineConfig,
|
||||
policyUpdateConfirmationRequest,
|
||||
excludeTools,
|
||||
toolDiscoveryCommand: settings.tools?.discoveryCommand,
|
||||
toolCallCommand: settings.tools?.callCommand,
|
||||
toolDiscoveryCommand: settings.tools.discoveryCommand,
|
||||
toolCallCommand: settings.tools.callCommand,
|
||||
mcpServerCommand,
|
||||
mcpServers,
|
||||
mcpEnablementCallbacks,
|
||||
@@ -785,32 +785,32 @@ export async function loadCliConfig(
|
||||
agents: settings.agents,
|
||||
adminSkillsEnabled,
|
||||
allowedMcpServers: mcpEnabled
|
||||
? (argv.allowedMcpServerNames ?? settings.mcp?.allowed)
|
||||
? (argv.allowedMcpServerNames ?? settings.mcp.allowed)
|
||||
: undefined,
|
||||
blockedMcpServers: mcpEnabled
|
||||
? argv.allowedMcpServerNames
|
||||
? undefined
|
||||
: settings.mcp?.excluded
|
||||
: settings.mcp.excluded
|
||||
: undefined,
|
||||
blockedEnvironmentVariables:
|
||||
settings.security?.environmentVariableRedaction?.blocked,
|
||||
settings.security.environmentVariableRedaction.blocked,
|
||||
enableEnvironmentVariableRedaction:
|
||||
settings.security?.environmentVariableRedaction?.enabled,
|
||||
settings.security.environmentVariableRedaction.enabled,
|
||||
userMemory: memoryContent,
|
||||
geminiMdFileCount: fileCount,
|
||||
geminiMdFilePaths: filePaths,
|
||||
approvalMode,
|
||||
disableYoloMode:
|
||||
settings.security?.disableYoloMode || settings.admin?.secureModeEnabled,
|
||||
showMemoryUsage: settings.ui?.showMemoryUsage || false,
|
||||
settings.security.disableYoloMode || settings.admin.secureModeEnabled,
|
||||
showMemoryUsage: settings.ui.showMemoryUsage,
|
||||
accessibility: {
|
||||
...settings.ui?.accessibility,
|
||||
...settings.ui.accessibility,
|
||||
screenReader,
|
||||
},
|
||||
telemetry: telemetrySettings,
|
||||
usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled,
|
||||
usageStatisticsEnabled: settings.privacy.usageStatisticsEnabled,
|
||||
fileFiltering,
|
||||
checkpointing: settings.general?.checkpointing?.enabled,
|
||||
checkpointing: settings.general.checkpointing.enabled,
|
||||
proxy:
|
||||
process.env['HTTPS_PROXY'] ||
|
||||
process.env['https_proxy'] ||
|
||||
|
||||
@@ -154,11 +154,8 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
installMetadata: ExtensionInstallMetadata,
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
): Promise<GeminiCLIExtension> {
|
||||
if (
|
||||
this.settings.security?.allowedExtensions &&
|
||||
this.settings.security?.allowedExtensions.length > 0
|
||||
) {
|
||||
const extensionAllowed = this.settings.security?.allowedExtensions.some(
|
||||
if (this.settings.security.allowedExtensions.length > 0) {
|
||||
const extensionAllowed = this.settings.security.allowedExtensions.some(
|
||||
(pattern) => {
|
||||
try {
|
||||
return new RegExp(pattern).test(installMetadata.source);
|
||||
@@ -312,7 +309,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
const destinationPath = new ExtensionStorage(
|
||||
newExtensionName,
|
||||
).getExtensionDir();
|
||||
let previousSettings: Record<string, string> | undefined;
|
||||
let previousSettings: Record<string, string | undefined> | undefined;
|
||||
if (isUpdate) {
|
||||
previousSettings = await getEnvContents(
|
||||
previousExtensionConfig,
|
||||
@@ -626,19 +623,16 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
const installMetadata = loadInstallMetadata(extensionDir);
|
||||
let effectiveExtensionPath = extensionDir;
|
||||
if (
|
||||
this.settings.security?.allowedExtensions &&
|
||||
this.settings.security?.allowedExtensions.length > 0
|
||||
) {
|
||||
if (this.settings.security.allowedExtensions.length > 0) {
|
||||
if (!installMetadata?.source) {
|
||||
throw new Error(
|
||||
`Failed to load extension ${extensionDir}. The ${INSTALL_METADATA_FILENAME} file is missing or misconfigured.`,
|
||||
);
|
||||
}
|
||||
const extensionAllowed = this.settings.security?.allowedExtensions.some(
|
||||
const extensionAllowed = this.settings.security.allowedExtensions.some(
|
||||
(pattern) => {
|
||||
try {
|
||||
return new RegExp(pattern).test(installMetadata?.source);
|
||||
return new RegExp(pattern).test(installMetadata.source);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
|
||||
@@ -672,8 +666,8 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
const extensionId = getExtensionId(config, installMetadata);
|
||||
|
||||
let userSettings: Record<string, string> = {};
|
||||
let workspaceSettings: Record<string, string> = {};
|
||||
let userSettings: Record<string, string | undefined> = {};
|
||||
let workspaceSettings: Record<string, string | undefined> = {};
|
||||
|
||||
if (this.settings.experimental.extensionConfig) {
|
||||
userSettings = await getScopedEnvContents(
|
||||
|
||||
@@ -166,7 +166,7 @@ export class ExtensionEnablementManager {
|
||||
const extensionConfig = config[extensionName];
|
||||
// Extensions are enabled by default.
|
||||
let enabled = true;
|
||||
const allOverrides = extensionConfig?.overrides ?? [];
|
||||
const allOverrides = extensionConfig.overrides ?? [];
|
||||
for (const rule of allOverrides) {
|
||||
const override = Override.fromFileRule(rule);
|
||||
if (override.matchesPath(ensureLeadingAndTrailingSlash(currentPath))) {
|
||||
|
||||
@@ -64,7 +64,7 @@ export async function maybePromptForSettings(
|
||||
extensionId: string,
|
||||
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
previousSettings?: Record<string, string>,
|
||||
previousSettings?: Record<string, string | undefined>,
|
||||
): Promise<void> {
|
||||
const { name: extensionName, settings } = extensionConfig;
|
||||
if (
|
||||
@@ -92,7 +92,9 @@ export async function maybePromptForSettings(
|
||||
previousExtensionConfig?.settings ?? [],
|
||||
);
|
||||
|
||||
const allSettings: Record<string, string> = { ...previousSettings };
|
||||
const allSettings: Record<string, string | undefined> = {
|
||||
...previousSettings,
|
||||
};
|
||||
|
||||
for (const removedEnvSetting of settingsChanges.removeEnv) {
|
||||
delete allSettings[removedEnvSetting.envVar];
|
||||
@@ -174,13 +176,13 @@ export async function getScopedEnvContents(
|
||||
extensionId: string,
|
||||
scope: ExtensionSettingScope,
|
||||
workspaceDir?: string,
|
||||
): Promise<Record<string, string>> {
|
||||
): Promise<Record<string, string | undefined>> {
|
||||
const { name: extensionName } = extensionConfig;
|
||||
const keychain = new KeychainTokenStorage(
|
||||
getKeychainStorageName(extensionName, extensionId, scope, workspaceDir),
|
||||
);
|
||||
const envFilePath = getEnvFilePath(extensionName, scope, workspaceDir);
|
||||
let customEnv: Record<string, string> = {};
|
||||
let customEnv: Record<string, string | undefined> = {};
|
||||
if (fsSync.existsSync(envFilePath)) {
|
||||
const stat = fsSync.statSync(envFilePath);
|
||||
if (!stat.isDirectory()) {
|
||||
@@ -206,7 +208,7 @@ export async function getEnvContents(
|
||||
extensionConfig: ExtensionConfig,
|
||||
extensionId: string,
|
||||
workspaceDir: string,
|
||||
): Promise<Record<string, string>> {
|
||||
): Promise<Record<string, string | undefined>> {
|
||||
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
@@ -109,16 +109,16 @@ export function tryParseGithubUrl(source: string): GithubRepoInfo | null {
|
||||
if (!parsedUrl) {
|
||||
throw new Error(`Invalid repo URL: ${source}`);
|
||||
}
|
||||
if (parsedUrl?.host !== 'github.com') {
|
||||
if (parsedUrl.host !== 'github.com') {
|
||||
return null;
|
||||
}
|
||||
// The pathname should be "/owner/repo".
|
||||
const parts = parsedUrl?.pathname
|
||||
const parts = parsedUrl.pathname
|
||||
.split('/')
|
||||
// Remove the empty segments, fixes trailing and leading slashes
|
||||
.filter((part) => part !== '');
|
||||
|
||||
if (parts?.length !== 2) {
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(
|
||||
`Invalid GitHub repository source: ${source}. Expected "owner/repo" or a github repo uri.`,
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function updateExtension(
|
||||
`Extension ${extension.name} cannot be updated, type is unknown.`,
|
||||
);
|
||||
}
|
||||
if (installMetadata?.type === 'link') {
|
||||
if (installMetadata.type === 'link') {
|
||||
dispatchExtensionStateUpdate({
|
||||
type: 'SET_STATE',
|
||||
payload: { name: extension.name, state: ExtensionUpdateState.UP_TO_DATE },
|
||||
|
||||
@@ -226,7 +226,7 @@ export class McpServerEnablementManager {
|
||||
async isFileEnabled(serverName: string): Promise<boolean> {
|
||||
const config = await this.readConfig();
|
||||
const state = config[normalizeServerId(serverName)];
|
||||
return state?.enabled ?? true;
|
||||
return state.enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,7 +45,7 @@ function getSandboxCommand(
|
||||
const environmentConfiguredSandbox =
|
||||
process.env['GEMINI_SANDBOX']?.toLowerCase().trim() ?? '';
|
||||
sandbox =
|
||||
environmentConfiguredSandbox?.length > 0
|
||||
environmentConfiguredSandbox.length > 0
|
||||
? environmentConfiguredSandbox
|
||||
: sandbox;
|
||||
if (sandbox === '1' || sandbox === 'true') sandbox = true;
|
||||
|
||||
@@ -460,7 +460,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.security?.folderTrust?.enabled).toBe(false); // Workspace setting should be used
|
||||
expect(settings.merged.security.folderTrust?.enabled).toBe(false); // Workspace setting should be used
|
||||
});
|
||||
|
||||
it('should use system folderTrust over user setting', () => {
|
||||
@@ -500,7 +500,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used
|
||||
expect(settings.merged.security.folderTrust?.enabled).toBe(true); // System setting should be used
|
||||
});
|
||||
|
||||
it('should not allow user or workspace to override system disableYoloMode', () => {
|
||||
@@ -534,7 +534,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used
|
||||
expect(settings.merged.security.disableYoloMode).toBe(true); // System setting should be used
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -630,7 +630,7 @@ describe('Settings Loading and Merging', () => {
|
||||
'WORKSPACE_DEBUG',
|
||||
'WORKSPACE_VAR',
|
||||
]);
|
||||
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
|
||||
expect(settings.merged.advanced.excludedEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
'NODE_ENV',
|
||||
@@ -658,7 +658,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.context?.fileName).toBeUndefined();
|
||||
expect(settings.merged.context.fileName).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -991,7 +991,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.model?.compressionThreshold).toEqual(expected);
|
||||
expect(settings.merged.model.compressionThreshold).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1018,7 +1018,7 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.merged.model?.compressionThreshold).toEqual(0.5);
|
||||
expect(settings.merged.model.compressionThreshold).toEqual(0.5);
|
||||
});
|
||||
|
||||
it('should merge includeDirectories from all scopes', () => {
|
||||
@@ -1052,7 +1052,7 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.merged.context?.includeDirectories).toEqual([
|
||||
expect(settings.merged.context.includeDirectories).toEqual([
|
||||
'/system/defaults/dir',
|
||||
'/user/dir1',
|
||||
'/user/dir2',
|
||||
@@ -1247,7 +1247,7 @@ describe('Settings Loading and Merging', () => {
|
||||
expect((settings.merged as TestSettings)['workspaceOnly']).toBe(
|
||||
'workspace_value',
|
||||
);
|
||||
expect(settings.merged.ui?.theme).toBe('light'); // workspace overrides user
|
||||
expect(settings.merged.ui.theme).toBe('light'); // workspace overrides user
|
||||
|
||||
delete process.env['SYSTEM_VAR'];
|
||||
delete process.env['USER_VAR'];
|
||||
@@ -1275,7 +1275,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim');
|
||||
expect(settings.merged.advanced.dnsResolutionOrder).toBe('verbatim');
|
||||
});
|
||||
|
||||
it('should use user dnsResolutionOrder if workspace is not defined', () => {
|
||||
@@ -1294,7 +1294,7 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim');
|
||||
expect(settings.merged.advanced.dnsResolutionOrder).toBe('verbatim');
|
||||
});
|
||||
|
||||
it('should leave unresolved environment variables as is', () => {
|
||||
@@ -1599,7 +1599,7 @@ describe('Settings Loading and Merging', () => {
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Verify the settings were loaded correctly
|
||||
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
|
||||
expect(settings.merged.advanced.excludedEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
]);
|
||||
@@ -1637,7 +1637,7 @@ describe('Settings Loading and Merging', () => {
|
||||
'NODE_ENV',
|
||||
'DEBUG',
|
||||
]);
|
||||
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
|
||||
expect(settings.merged.advanced.excludedEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
'NODE_ENV',
|
||||
@@ -1677,7 +1677,7 @@ describe('Settings Loading and Merging', () => {
|
||||
'WORKSPACE_DEBUG',
|
||||
'WORKSPACE_VAR',
|
||||
]);
|
||||
expect(settings.merged.advanced?.excludedEnvVars).toEqual([
|
||||
expect(settings.merged.advanced.excludedEnvVars).toEqual([
|
||||
'DEBUG',
|
||||
'DEBUG_MODE',
|
||||
'NODE_ENV',
|
||||
@@ -1711,9 +1711,9 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.tools?.sandbox).toBe(true);
|
||||
expect(settings.merged.context?.fileName).toBe('WORKSPACE.md');
|
||||
expect(settings.merged.ui?.theme).toBe('dark');
|
||||
expect(settings.merged.tools.sandbox).toBe(true);
|
||||
expect(settings.merged.context.fileName).toBe('WORKSPACE.md');
|
||||
expect(settings.merged.ui.theme).toBe('dark');
|
||||
});
|
||||
|
||||
it('should NOT merge workspace settings when workspace is not trusted', () => {
|
||||
@@ -1744,9 +1744,9 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.merged.tools?.sandbox).toBe(false); // User setting
|
||||
expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting
|
||||
expect(settings.merged.ui?.theme).toBe('dark'); // User setting
|
||||
expect(settings.merged.tools.sandbox).toBe(false); // User setting
|
||||
expect(settings.merged.context.fileName).toBe('USER.md'); // User setting
|
||||
expect(settings.merged.ui.theme).toBe('dark'); // User setting
|
||||
});
|
||||
|
||||
it('should NOT merge workspace settings when workspace trust is undefined', () => {
|
||||
@@ -1777,8 +1777,8 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(settings.merged.tools?.sandbox).toBe(false); // User setting
|
||||
expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting
|
||||
expect(settings.merged.tools.sandbox).toBe(false); // User setting
|
||||
expect(settings.merged.context.fileName).toBe('USER.md'); // User setting
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2229,7 +2229,7 @@ describe('Settings Loading and Merging', () => {
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Verify it was migrated in the merged settings
|
||||
expect(settings.merged.general?.enableAutoUpdate).toBe(false);
|
||||
expect(settings.merged.general.enableAutoUpdate).toBe(false);
|
||||
|
||||
// Verify it was saved back to disk (via setValue calling updateSettingsFilePreservingFormat)
|
||||
expect(updateSettingsFilePreservingFormat).toHaveBeenCalledWith(
|
||||
@@ -2289,7 +2289,7 @@ describe('Settings Loading and Merging', () => {
|
||||
).toBe(true);
|
||||
|
||||
// Merged should also reflect it (system overrides defaults, but both are migrated)
|
||||
expect(settings.merged.general?.enableAutoUpdateNotification).toBe(false);
|
||||
expect(settings.merged.general.enableAutoUpdateNotification).toBe(false);
|
||||
|
||||
// Verify it was NOT saved back to disk
|
||||
expect(updateSettingsFilePreservingFormat).not.toHaveBeenCalledWith(
|
||||
@@ -2487,10 +2487,10 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
// 1. Verify that on initial load, file-based admin settings are ignored
|
||||
// and schema defaults are used instead.
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false); // default: false
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true); // default: true
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true); // default: true
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('system-theme'); // non-admin setting should be loaded
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false); // default: false
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true); // default: true
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true); // default: true
|
||||
expect(loadedSettings.merged.ui.theme).toBe('system-theme'); // non-admin setting should be loaded
|
||||
|
||||
// 2. Now, set remote admin settings.
|
||||
loadedSettings.setRemoteAdminSettings({
|
||||
@@ -2503,11 +2503,11 @@ describe('Settings Loading and Merging', () => {
|
||||
});
|
||||
|
||||
// 3. Verify that remote admin settings take precedence.
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(false);
|
||||
// non-admin setting should remain unchanged
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('system-theme');
|
||||
expect(loadedSettings.merged.ui.theme).toBe('system-theme');
|
||||
});
|
||||
|
||||
it('should set remote admin settings and recompute merged settings', () => {
|
||||
@@ -2532,10 +2532,10 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
// Ensure initial state from defaults (as file-based admin settings are ignored)
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.ui.theme).toBe('initial-theme');
|
||||
|
||||
const newRemoteSettings = {
|
||||
strictModeDisabled: false,
|
||||
@@ -2549,11 +2549,11 @@ describe('Settings Loading and Merging', () => {
|
||||
loadedSettings.setRemoteAdminSettings(newRemoteSettings);
|
||||
|
||||
// Verify that remote admin settings are applied
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(false);
|
||||
// Non-admin settings should remain untouched
|
||||
expect(loadedSettings.merged.ui?.theme).toBe('initial-theme');
|
||||
expect(loadedSettings.merged.ui.theme).toBe('initial-theme');
|
||||
});
|
||||
|
||||
it('should correctly handle undefined remote admin settings', () => {
|
||||
@@ -2573,16 +2573,16 @@ describe('Settings Loading and Merging', () => {
|
||||
|
||||
const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
// Should have default admin settings
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
|
||||
|
||||
loadedSettings.setRemoteAdminSettings({}); // Set empty remote settings
|
||||
|
||||
// Admin settings should revert to defaults because there are no remote overrides
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should un-nest MCP configuration from remote settings', () => {
|
||||
@@ -2604,7 +2604,7 @@ describe('Settings Loading and Merging', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(loadedSettings.merged.admin?.mcp?.config).toEqual(mcpServers);
|
||||
expect(loadedSettings.merged.admin.mcp?.config).toEqual(mcpServers);
|
||||
});
|
||||
|
||||
it('should set skills based on unmanagedCapabilitiesEnabled', () => {
|
||||
@@ -2630,9 +2630,9 @@ describe('Settings Loading and Merging', () => {
|
||||
loadedSettings.setRemoteAdminSettings({});
|
||||
|
||||
// Should default to schema defaults (standard defaults)
|
||||
expect(loadedSettings.merged.admin?.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin?.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin?.extensions?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.secureModeEnabled).toBe(false);
|
||||
expect(loadedSettings.merged.admin.mcp?.enabled).toBe(true);
|
||||
expect(loadedSettings.merged.admin.extensions?.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -54,24 +54,20 @@ import {
|
||||
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]) {
|
||||
const current: SettingDefinition | undefined = currentSchema?.[key];
|
||||
if (!current) {
|
||||
// Key not found in schema - check if parent has additionalProperties
|
||||
if (parent?.additionalProperties?.mergeStrategy) {
|
||||
return parent.additionalProperties.mergeStrategy;
|
||||
}
|
||||
return undefined;
|
||||
return parent?.additionalProperties?.mergeStrategy;
|
||||
}
|
||||
parent = current;
|
||||
current = currentSchema[key];
|
||||
currentSchema = current.properties;
|
||||
}
|
||||
|
||||
return current?.mergeStrategy;
|
||||
return parent?.mergeStrategy;
|
||||
}
|
||||
|
||||
export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
|
||||
@@ -372,19 +368,17 @@ export class LoadedSettings {
|
||||
// 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);
|
||||
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'];
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -493,7 +487,7 @@ export class LoadedSettings {
|
||||
|
||||
function findEnvFile(startDir: string): string | null {
|
||||
let currentDir = path.resolve(startDir);
|
||||
while (true) {
|
||||
for (;;) {
|
||||
// prefer gemini-specific .env under GEMINI_DIR
|
||||
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
|
||||
if (fs.existsSync(geminiEnvPath)) {
|
||||
@@ -583,7 +577,7 @@ export function loadEnvironment(
|
||||
const parsedEnv = dotenv.parse(envFileContent);
|
||||
|
||||
const excludedVars =
|
||||
settings?.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
|
||||
settings.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
|
||||
const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR);
|
||||
|
||||
for (const key in parsedEnv) {
|
||||
@@ -1085,7 +1079,7 @@ function migrateExperimentalSettings(
|
||||
};
|
||||
const agentsOverrides = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
...((agentsSettings['overrides'] as Record<string, unknown>) || {}),
|
||||
...(agentsSettings['overrides'] as Record<string, unknown>),
|
||||
};
|
||||
let modified = false;
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ describe('Settings Repro', () => {
|
||||
// If it doesn't throw, check if it merged correctly.
|
||||
// The model.compressionThreshold should be present.
|
||||
// And model.name should probably be undefined or default, but certainly NOT { compressionThreshold: 0.8 }
|
||||
expect(settings.merged.model?.compressionThreshold).toBe(0.8);
|
||||
expect(typeof settings.merged.model?.name).not.toBe('object');
|
||||
expect(settings.merged.model.compressionThreshold).toBe(0.8);
|
||||
expect(typeof settings.merged.model.name).not.toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user