Add an experimental setting for extension config (#16506)

This commit is contained in:
christine betts
2026-01-14 10:16:42 -05:00
committed by GitHub
parent f6c2d61906
commit 3b55581aaf
8 changed files with 85 additions and 22 deletions

View File

@@ -830,6 +830,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true` - **Default:** `true`
- **Requires restart:** Yes - **Requires restart:** Yes
- **`experimental.extensionConfig`** (boolean):
- **Description:** Enable requesting and fetching of extension settings.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.extensionReloading`** (boolean): - **`experimental.extensionReloading`** (boolean):
- **Description:** Enables extension loading/unloading within the CLI session. - **Description:** Enables extension loading/unloading within the CLI session.
- **Default:** `false` - **Default:** `false`

View File

@@ -12,7 +12,8 @@ import {
getScopedEnvContents, getScopedEnvContents,
} from '../../config/extensions/extensionSettings.js'; } from '../../config/extensions/extensionSettings.js';
import { getExtensionAndManager, getExtensionManager } from './utils.js'; import { getExtensionAndManager, getExtensionManager } from './utils.js';
import { debugLogger } from '@google/gemini-cli-core'; import { loadSettings } from '../../config/settings.js';
import { debugLogger, coreEvents } from '@google/gemini-cli-core';
import { exitCli } from '../utils.js'; import { exitCli } from '../utils.js';
import prompts from 'prompts'; import prompts from 'prompts';
import type { ExtensionConfig } from '../../config/extension.js'; import type { ExtensionConfig } from '../../config/extension.js';
@@ -43,6 +44,16 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
}), }),
handler: async (args) => { handler: async (args) => {
const { name, setting, scope } = args; const { name, setting, scope } = args;
const settings = loadSettings(process.cwd()).merged;
if (!(settings.experimental?.extensionConfig ?? true)) {
coreEvents.emitFeedback(
'error',
'Extension configuration is currently disabled. Enable it by setting "experimental.extensionConfig" to true.',
);
await exitCli();
return;
}
if (name) { if (name) {
if (name.includes('/') || name.includes('\\') || name.includes('..')) { if (name.includes('/') || name.includes('\\') || name.includes('..')) {

View File

@@ -109,6 +109,9 @@ describe('ExtensionManager Settings Scope', () => {
telemetry: { telemetry: {
enabled: false, enabled: false,
}, },
experimental: {
extensionConfig: true,
},
} as Settings, } as Settings,
}); });
@@ -148,6 +151,9 @@ describe('ExtensionManager Settings Scope', () => {
telemetry: { telemetry: {
enabled: false, enabled: false,
}, },
experimental: {
extensionConfig: true,
},
} as Settings, } as Settings,
}); });
@@ -185,6 +191,9 @@ describe('ExtensionManager Settings Scope', () => {
telemetry: { telemetry: {
enabled: false, enabled: false,
}, },
experimental: {
extensionConfig: true,
},
} as Settings, } as Settings,
}); });

View File

@@ -287,7 +287,10 @@ Would you like to attempt to install via "git clone" instead?`,
} }
await fs.promises.mkdir(destinationPath, { recursive: true }); await fs.promises.mkdir(destinationPath, { recursive: true });
if (this.requestSetting) { if (
this.requestSetting &&
(this.settings.experimental?.extensionConfig ?? false)
) {
if (isUpdate) { if (isUpdate) {
await maybePromptForSettings( await maybePromptForSettings(
newExtensionConfig, newExtensionConfig,
@@ -305,11 +308,14 @@ Would you like to attempt to install via "git clone" instead?`,
} }
} }
const missingSettings = await getMissingSettings( const missingSettings =
newExtensionConfig, (this.settings.experimental?.extensionConfig ?? false)
extensionId, ? await getMissingSettings(
this.workspaceDir, newExtensionConfig,
); extensionId,
this.workspaceDir,
)
: [];
if (missingSettings.length > 0) { if (missingSettings.length > 0) {
const message = `Extension "${newExtensionConfig.name}" has missing settings: ${missingSettings const message = `Extension "${newExtensionConfig.name}" has missing settings: ${missingSettings
.map((s) => s.name) .map((s) => s.name)
@@ -526,23 +532,31 @@ Would you like to attempt to install via "git clone" instead?`,
const extensionId = getExtensionId(config, installMetadata); const extensionId = getExtensionId(config, installMetadata);
const userSettings = await getScopedEnvContents( let userSettings: Record<string, string> = {};
config, let workspaceSettings: Record<string, string> = {};
extensionId,
ExtensionSettingScope.USER, if (this.settings.experimental?.extensionConfig ?? false) {
); userSettings = await getScopedEnvContents(
const workspaceSettings = await getScopedEnvContents( config,
config, extensionId,
extensionId, ExtensionSettingScope.USER,
ExtensionSettingScope.WORKSPACE, );
this.workspaceDir, workspaceSettings = await getScopedEnvContents(
); config,
extensionId,
ExtensionSettingScope.WORKSPACE,
this.workspaceDir,
);
}
const customEnv = { ...userSettings, ...workspaceSettings }; const customEnv = { ...userSettings, ...workspaceSettings };
config = resolveEnvVarsInObject(config, customEnv); config = resolveEnvVarsInObject(config, customEnv);
const resolvedSettings: ResolvedExtensionSetting[] = []; const resolvedSettings: ResolvedExtensionSetting[] = [];
if (config.settings) { if (
config.settings &&
(this.settings.experimental?.extensionConfig ?? false)
) {
for (const setting of config.settings) { for (const setting of config.settings) {
const value = customEnv[setting.envVar]; const value = customEnv[setting.envVar];
let scope: 'user' | 'workspace' | undefined; let scope: 'user' | 'workspace' | undefined;

View File

@@ -200,11 +200,13 @@ describe('extension tests', () => {
source: undefined, source: undefined,
}); });
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir); vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
const settings = loadSettings(tempWorkspaceDir).merged;
(settings.experimental ??= {}).extensionConfig = true;
extensionManager = new ExtensionManager({ extensionManager = new ExtensionManager({
workspaceDir: tempWorkspaceDir, workspaceDir: tempWorkspaceDir,
requestConsent: mockRequestConsent, requestConsent: mockRequestConsent,
requestSetting: mockPromptForSettings, requestSetting: mockPromptForSettings,
settings: loadSettings(tempWorkspaceDir).merged, settings,
}); });
resetTrustedFoldersForTesting(); resetTrustedFoldersForTesting();
}); });

View File

@@ -11,6 +11,7 @@ import * as fs from 'node:fs';
import { getMissingSettings } from './extensionSettings.js'; import { getMissingSettings } from './extensionSettings.js';
import type { ExtensionConfig } from '../extension.js'; import type { ExtensionConfig } from '../extension.js';
import { ExtensionStorage } from './storage.js'; import { ExtensionStorage } from './storage.js';
import type { Settings } from '../settings.js';
import { import {
KeychainTokenStorage, KeychainTokenStorage,
debugLogger, debugLogger,
@@ -245,8 +246,13 @@ describe('extensionUpdates', () => {
const manager = new ExtensionManager({ const manager = new ExtensionManager({
workspaceDir: tempWorkspaceDir, workspaceDir: tempWorkspaceDir,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settings: { telemetry: {} } as any, settings: {
telemetry: {
enabled: false,
},
experimental: { extensionConfig: true },
} as unknown as Settings,
requestConsent: vi.fn().mockResolvedValue(true), requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: null, // Simulate non-interactive requestSetting: null, // Simulate non-interactive
}); });

View File

@@ -1413,6 +1413,15 @@ const SETTINGS_SCHEMA = {
description: 'Enable extension management features.', description: 'Enable extension management features.',
showInDialog: false, showInDialog: false,
}, },
extensionConfig: {
type: 'boolean',
label: 'Extension Configuration',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Enable requesting and fetching of extension settings.',
showInDialog: false,
},
extensionReloading: { extensionReloading: {
type: 'boolean', type: 'boolean',
label: 'Extension Reloading', label: 'Extension Reloading',

View File

@@ -1393,6 +1393,13 @@
"default": true, "default": true,
"type": "boolean" "type": "boolean"
}, },
"extensionConfig": {
"title": "Extension Configuration",
"description": "Enable requesting and fetching of extension settings.",
"markdownDescription": "Enable requesting and fetching of extension settings.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"extensionReloading": { "extensionReloading": {
"title": "Extension Reloading", "title": "Extension Reloading",
"description": "Enables extension loading/unloading within the CLI session.", "description": "Enables extension loading/unloading within the CLI session.",