mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
Show settings source in extensions lists (#16207)
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import { ExtensionManager } from './extension-manager.js';
|
||||||
|
import type { Settings } from './settings.js';
|
||||||
|
|
||||||
|
let currentTempHome = '';
|
||||||
|
|
||||||
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
homedir: () => currentTempHome,
|
||||||
|
debugLogger: {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ExtensionManager Settings Scope', () => {
|
||||||
|
const extensionName = 'test-extension';
|
||||||
|
let tempWorkspace: string;
|
||||||
|
let extensionsDir: string;
|
||||||
|
let extensionDir: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentTempHome = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
|
||||||
|
);
|
||||||
|
tempWorkspace = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'gemini-cli-test-workspace-'),
|
||||||
|
);
|
||||||
|
extensionsDir = path.join(currentTempHome, '.gemini', 'extensions');
|
||||||
|
extensionDir = path.join(extensionsDir, extensionName);
|
||||||
|
|
||||||
|
fs.mkdirSync(extensionDir, { recursive: true });
|
||||||
|
|
||||||
|
// Create gemini-extension.json
|
||||||
|
const extensionConfig = {
|
||||||
|
name: extensionName,
|
||||||
|
version: '1.0.0',
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
name: 'Test Setting',
|
||||||
|
envVar: 'TEST_SETTING',
|
||||||
|
description: 'A test setting',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(extensionDir, 'gemini-extension.json'),
|
||||||
|
JSON.stringify(extensionConfig),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create install metadata
|
||||||
|
const installMetadata = {
|
||||||
|
source: extensionDir,
|
||||||
|
type: 'local',
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(extensionDir, 'install-metadata.json'),
|
||||||
|
JSON.stringify(installMetadata),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up files if needed, or rely on temp dir cleanup
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize workspace settings over user settings and report correct scope', async () => {
|
||||||
|
// 1. Set User Setting
|
||||||
|
const userSettingsPath = path.join(extensionDir, '.env');
|
||||||
|
fs.writeFileSync(userSettingsPath, 'TEST_SETTING=user-value');
|
||||||
|
|
||||||
|
// 2. Set Workspace Setting
|
||||||
|
const workspaceSettingsPath = path.join(tempWorkspace, '.env');
|
||||||
|
fs.writeFileSync(workspaceSettingsPath, 'TEST_SETTING=workspace-value');
|
||||||
|
|
||||||
|
const extensionManager = new ExtensionManager({
|
||||||
|
workspaceDir: tempWorkspace,
|
||||||
|
requestConsent: async () => true,
|
||||||
|
requestSetting: async () => '',
|
||||||
|
settings: {
|
||||||
|
telemetry: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
} as Settings,
|
||||||
|
});
|
||||||
|
|
||||||
|
const extensions = await extensionManager.loadExtensions();
|
||||||
|
const extension = extensions.find((e) => e.name === extensionName);
|
||||||
|
|
||||||
|
expect(extension).toBeDefined();
|
||||||
|
|
||||||
|
// Verify resolved settings
|
||||||
|
const setting = extension?.resolvedSettings?.find(
|
||||||
|
(s) => s.envVar === 'TEST_SETTING',
|
||||||
|
);
|
||||||
|
expect(setting).toBeDefined();
|
||||||
|
expect(setting?.value).toBe('workspace-value');
|
||||||
|
expect(setting?.scope).toBe('workspace');
|
||||||
|
expect(setting?.source).toBe(workspaceSettingsPath);
|
||||||
|
|
||||||
|
// Verify output string contains (Workspace - <path>)
|
||||||
|
const output = extensionManager.toOutputString(extension!);
|
||||||
|
expect(output).toContain(
|
||||||
|
`Test Setting: workspace-value (Workspace - ${workspaceSettingsPath})`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to user settings if workspace setting is missing', async () => {
|
||||||
|
// 1. Set User Setting
|
||||||
|
const userSettingsPath = path.join(extensionDir, '.env');
|
||||||
|
fs.writeFileSync(userSettingsPath, 'TEST_SETTING=user-value');
|
||||||
|
|
||||||
|
// 2. No Workspace Setting
|
||||||
|
|
||||||
|
const extensionManager = new ExtensionManager({
|
||||||
|
workspaceDir: tempWorkspace,
|
||||||
|
requestConsent: async () => true,
|
||||||
|
requestSetting: async () => '',
|
||||||
|
settings: {
|
||||||
|
telemetry: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
} as Settings,
|
||||||
|
});
|
||||||
|
|
||||||
|
const extensions = await extensionManager.loadExtensions();
|
||||||
|
const extension = extensions.find((e) => e.name === extensionName);
|
||||||
|
|
||||||
|
expect(extension).toBeDefined();
|
||||||
|
|
||||||
|
// Verify resolved settings
|
||||||
|
const setting = extension?.resolvedSettings?.find(
|
||||||
|
(s) => s.envVar === 'TEST_SETTING',
|
||||||
|
);
|
||||||
|
expect(setting).toBeDefined();
|
||||||
|
expect(setting?.value).toBe('user-value');
|
||||||
|
expect(setting?.scope).toBe('user');
|
||||||
|
expect(setting?.source?.endsWith(path.join(extensionName, '.env'))).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify output string contains (User - <path>)
|
||||||
|
const output = extensionManager.toOutputString(extension!);
|
||||||
|
expect(output).toContain(
|
||||||
|
`Test Setting: user-value (User - ${userSettingsPath})`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report unset if neither is present', async () => {
|
||||||
|
// No settings files
|
||||||
|
|
||||||
|
const extensionManager = new ExtensionManager({
|
||||||
|
workspaceDir: tempWorkspace,
|
||||||
|
requestConsent: async () => true,
|
||||||
|
requestSetting: async () => '',
|
||||||
|
settings: {
|
||||||
|
telemetry: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
} as Settings,
|
||||||
|
});
|
||||||
|
|
||||||
|
const extensions = await extensionManager.loadExtensions();
|
||||||
|
const extension = extensions.find((e) => e.name === extensionName);
|
||||||
|
|
||||||
|
expect(extension).toBeDefined();
|
||||||
|
|
||||||
|
// Verify resolved settings
|
||||||
|
const setting = extension?.resolvedSettings?.find(
|
||||||
|
(s) => s.envVar === 'TEST_SETTING',
|
||||||
|
);
|
||||||
|
expect(setting).toBeDefined();
|
||||||
|
expect(setting?.value).toBe('[not set]');
|
||||||
|
expect(setting?.scope).toBeUndefined();
|
||||||
|
|
||||||
|
// Verify output string does not contain scope
|
||||||
|
const output = extensionManager.toOutputString(extension!);
|
||||||
|
expect(output).toContain('Test Setting: [not set]');
|
||||||
|
expect(output).not.toContain('Test Setting: [not set] (User)');
|
||||||
|
expect(output).not.toContain('Test Setting: [not set] (Workspace)');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -59,9 +59,12 @@ import {
|
|||||||
} from './extensions/variables.js';
|
} from './extensions/variables.js';
|
||||||
import {
|
import {
|
||||||
getEnvContents,
|
getEnvContents,
|
||||||
|
getEnvFilePath,
|
||||||
maybePromptForSettings,
|
maybePromptForSettings,
|
||||||
getMissingSettings,
|
getMissingSettings,
|
||||||
type ExtensionSetting,
|
type ExtensionSetting,
|
||||||
|
getScopedEnvContents,
|
||||||
|
ExtensionSettingScope,
|
||||||
} from './extensions/extensionSettings.js';
|
} from './extensions/extensionSettings.js';
|
||||||
import type { EventEmitter } from 'node:stream';
|
import type { EventEmitter } from 'node:stream';
|
||||||
import { getEnableHooks } from './settingsSchema.js';
|
import { getEnableHooks } from './settingsSchema.js';
|
||||||
@@ -277,6 +280,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
previousSettings = await getEnvContents(
|
previousSettings = await getEnvContents(
|
||||||
previousExtensionConfig,
|
previousExtensionConfig,
|
||||||
extensionId,
|
extensionId,
|
||||||
|
this.workspaceDir,
|
||||||
);
|
);
|
||||||
await this.uninstallExtension(newExtensionName, isUpdate);
|
await this.uninstallExtension(newExtensionName, isUpdate);
|
||||||
}
|
}
|
||||||
@@ -303,6 +307,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
const missingSettings = await getMissingSettings(
|
const missingSettings = await getMissingSettings(
|
||||||
newExtensionConfig,
|
newExtensionConfig,
|
||||||
extensionId,
|
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
|
||||||
@@ -518,16 +523,51 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customEnv = await getEnvContents(
|
const extensionId = getExtensionId(config, installMetadata);
|
||||||
|
|
||||||
|
const userSettings = await getScopedEnvContents(
|
||||||
config,
|
config,
|
||||||
getExtensionId(config, installMetadata),
|
extensionId,
|
||||||
|
ExtensionSettingScope.USER,
|
||||||
);
|
);
|
||||||
|
const workspaceSettings = await getScopedEnvContents(
|
||||||
|
config,
|
||||||
|
extensionId,
|
||||||
|
ExtensionSettingScope.WORKSPACE,
|
||||||
|
this.workspaceDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
const customEnv = { ...userSettings, ...workspaceSettings };
|
||||||
config = resolveEnvVarsInObject(config, customEnv);
|
config = resolveEnvVarsInObject(config, customEnv);
|
||||||
|
|
||||||
const resolvedSettings: ResolvedExtensionSetting[] = [];
|
const resolvedSettings: ResolvedExtensionSetting[] = [];
|
||||||
if (config.settings) {
|
if (config.settings) {
|
||||||
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 source: string | undefined;
|
||||||
|
|
||||||
|
// Note: strict check for undefined, as empty string is a valid value
|
||||||
|
if (workspaceSettings[setting.envVar] !== undefined) {
|
||||||
|
scope = 'workspace';
|
||||||
|
if (setting.sensitive) {
|
||||||
|
source = 'Keychain';
|
||||||
|
} else {
|
||||||
|
source = getEnvFilePath(
|
||||||
|
config.name,
|
||||||
|
ExtensionSettingScope.WORKSPACE,
|
||||||
|
this.workspaceDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (userSettings[setting.envVar] !== undefined) {
|
||||||
|
scope = 'user';
|
||||||
|
if (setting.sensitive) {
|
||||||
|
source = 'Keychain';
|
||||||
|
} else {
|
||||||
|
source = getEnvFilePath(config.name, ExtensionSettingScope.USER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resolvedSettings.push({
|
resolvedSettings.push({
|
||||||
name: setting.name,
|
name: setting.name,
|
||||||
envVar: setting.envVar,
|
envVar: setting.envVar,
|
||||||
@@ -538,6 +578,8 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
? '***'
|
? '***'
|
||||||
: value,
|
: value,
|
||||||
sensitive: setting.sensitive ?? false,
|
sensitive: setting.sensitive ?? false,
|
||||||
|
scope,
|
||||||
|
source,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -754,7 +796,15 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
if (resolvedSettings && resolvedSettings.length > 0) {
|
if (resolvedSettings && resolvedSettings.length > 0) {
|
||||||
output += `\n Settings:`;
|
output += `\n Settings:`;
|
||||||
resolvedSettings.forEach((setting) => {
|
resolvedSettings.forEach((setting) => {
|
||||||
output += `\n ${setting.name}: ${setting.value}`;
|
let scope = '';
|
||||||
|
if (setting.scope) {
|
||||||
|
scope = setting.scope === 'workspace' ? '(Workspace' : '(User';
|
||||||
|
if (setting.source) {
|
||||||
|
scope += ` - ${setting.source}`;
|
||||||
|
}
|
||||||
|
scope += ')';
|
||||||
|
}
|
||||||
|
output += `\n ${setting.name}: ${setting.value} ${scope}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
|
|||||||
@@ -529,6 +529,7 @@ describe('extensionSettings', () => {
|
|||||||
config,
|
config,
|
||||||
extensionId,
|
extensionId,
|
||||||
ExtensionSettingScope.USER,
|
ExtensionSettingScope.USER,
|
||||||
|
tempWorkspaceDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(contents).toEqual({
|
expect(contents).toEqual({
|
||||||
@@ -552,6 +553,7 @@ describe('extensionSettings', () => {
|
|||||||
config,
|
config,
|
||||||
extensionId,
|
extensionId,
|
||||||
ExtensionSettingScope.WORKSPACE,
|
ExtensionSettingScope.WORKSPACE,
|
||||||
|
tempWorkspaceDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(contents).toEqual({
|
expect(contents).toEqual({
|
||||||
@@ -596,7 +598,11 @@ describe('extensionSettings', () => {
|
|||||||
);
|
);
|
||||||
await workspaceKeychain.setSecret('VAR2', 'workspace-secret2');
|
await workspaceKeychain.setSecret('VAR2', 'workspace-secret2');
|
||||||
|
|
||||||
const contents = await getEnvContents(config, extensionId);
|
const contents = await getEnvContents(
|
||||||
|
config,
|
||||||
|
extensionId,
|
||||||
|
tempWorkspaceDir,
|
||||||
|
);
|
||||||
|
|
||||||
expect(contents).toEqual({
|
expect(contents).toEqual({
|
||||||
VAR1: 'workspace-value1',
|
VAR1: 'workspace-value1',
|
||||||
@@ -636,6 +642,7 @@ describe('extensionSettings', () => {
|
|||||||
'VAR1',
|
'VAR1',
|
||||||
mockRequestSetting,
|
mockRequestSetting,
|
||||||
ExtensionSettingScope.USER,
|
ExtensionSettingScope.USER,
|
||||||
|
tempWorkspaceDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedEnvPath = path.join(extensionDir, '.env');
|
const expectedEnvPath = path.join(extensionDir, '.env');
|
||||||
@@ -652,6 +659,7 @@ describe('extensionSettings', () => {
|
|||||||
'VAR1',
|
'VAR1',
|
||||||
mockRequestSetting,
|
mockRequestSetting,
|
||||||
ExtensionSettingScope.WORKSPACE,
|
ExtensionSettingScope.WORKSPACE,
|
||||||
|
tempWorkspaceDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedEnvPath = path.join(tempWorkspaceDir, '.env');
|
const expectedEnvPath = path.join(tempWorkspaceDir, '.env');
|
||||||
@@ -668,6 +676,7 @@ describe('extensionSettings', () => {
|
|||||||
'VAR2',
|
'VAR2',
|
||||||
mockRequestSetting,
|
mockRequestSetting,
|
||||||
ExtensionSettingScope.USER,
|
ExtensionSettingScope.USER,
|
||||||
|
tempWorkspaceDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
const userKeychain = new KeychainTokenStorage(
|
const userKeychain = new KeychainTokenStorage(
|
||||||
@@ -685,6 +694,7 @@ describe('extensionSettings', () => {
|
|||||||
'VAR2',
|
'VAR2',
|
||||||
mockRequestSetting,
|
mockRequestSetting,
|
||||||
ExtensionSettingScope.WORKSPACE,
|
ExtensionSettingScope.WORKSPACE,
|
||||||
|
tempWorkspaceDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
const workspaceKeychain = new KeychainTokenStorage(
|
const workspaceKeychain = new KeychainTokenStorage(
|
||||||
@@ -710,6 +720,7 @@ describe('extensionSettings', () => {
|
|||||||
'VAR1',
|
'VAR1',
|
||||||
mockRequestSetting,
|
mockRequestSetting,
|
||||||
ExtensionSettingScope.WORKSPACE,
|
ExtensionSettingScope.WORKSPACE,
|
||||||
|
tempWorkspaceDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Read the .env file after update
|
// Read the .env file after update
|
||||||
|
|||||||
@@ -33,20 +33,28 @@ const getKeychainStorageName = (
|
|||||||
extensionName: string,
|
extensionName: string,
|
||||||
extensionId: string,
|
extensionId: string,
|
||||||
scope: ExtensionSettingScope,
|
scope: ExtensionSettingScope,
|
||||||
|
workspaceDir?: string,
|
||||||
): string => {
|
): string => {
|
||||||
const base = `Gemini CLI Extensions ${extensionName} ${extensionId}`;
|
const base = `Gemini CLI Extensions ${extensionName} ${extensionId}`;
|
||||||
if (scope === ExtensionSettingScope.WORKSPACE) {
|
if (scope === ExtensionSettingScope.WORKSPACE) {
|
||||||
return `${base} ${process.cwd()}`;
|
if (!workspaceDir) {
|
||||||
|
throw new Error('Workspace directory is required for workspace scope');
|
||||||
|
}
|
||||||
|
return `${base} ${workspaceDir}`;
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEnvFilePath = (
|
export const getEnvFilePath = (
|
||||||
extensionName: string,
|
extensionName: string,
|
||||||
scope: ExtensionSettingScope,
|
scope: ExtensionSettingScope,
|
||||||
|
workspaceDir?: string,
|
||||||
): string => {
|
): string => {
|
||||||
if (scope === ExtensionSettingScope.WORKSPACE) {
|
if (scope === ExtensionSettingScope.WORKSPACE) {
|
||||||
return path.join(process.cwd(), EXTENSION_SETTINGS_FILENAME);
|
if (!workspaceDir) {
|
||||||
|
throw new Error('Workspace directory is required for workspace scope');
|
||||||
|
}
|
||||||
|
return path.join(workspaceDir, EXTENSION_SETTINGS_FILENAME);
|
||||||
}
|
}
|
||||||
return new ExtensionStorage(extensionName).getEnvFilePath();
|
return new ExtensionStorage(extensionName).getEnvFilePath();
|
||||||
};
|
};
|
||||||
@@ -143,12 +151,13 @@ export async function getScopedEnvContents(
|
|||||||
extensionConfig: ExtensionConfig,
|
extensionConfig: ExtensionConfig,
|
||||||
extensionId: string,
|
extensionId: string,
|
||||||
scope: ExtensionSettingScope,
|
scope: ExtensionSettingScope,
|
||||||
|
workspaceDir?: string,
|
||||||
): Promise<Record<string, string>> {
|
): Promise<Record<string, string>> {
|
||||||
const { name: extensionName } = extensionConfig;
|
const { name: extensionName } = extensionConfig;
|
||||||
const keychain = new KeychainTokenStorage(
|
const keychain = new KeychainTokenStorage(
|
||||||
getKeychainStorageName(extensionName, extensionId, scope),
|
getKeychainStorageName(extensionName, extensionId, scope, workspaceDir),
|
||||||
);
|
);
|
||||||
const envFilePath = getEnvFilePath(extensionName, scope);
|
const envFilePath = getEnvFilePath(extensionName, scope, workspaceDir);
|
||||||
let customEnv: Record<string, string> = {};
|
let customEnv: Record<string, string> = {};
|
||||||
if (fsSync.existsSync(envFilePath)) {
|
if (fsSync.existsSync(envFilePath)) {
|
||||||
const envFile = fsSync.readFileSync(envFilePath, 'utf-8');
|
const envFile = fsSync.readFileSync(envFilePath, 'utf-8');
|
||||||
@@ -171,6 +180,7 @@ export async function getScopedEnvContents(
|
|||||||
export async function getEnvContents(
|
export async function getEnvContents(
|
||||||
extensionConfig: ExtensionConfig,
|
extensionConfig: ExtensionConfig,
|
||||||
extensionId: string,
|
extensionId: string,
|
||||||
|
workspaceDir: string,
|
||||||
): Promise<Record<string, string>> {
|
): Promise<Record<string, string>> {
|
||||||
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
|
if (!extensionConfig.settings || extensionConfig.settings.length === 0) {
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
@@ -185,6 +195,7 @@ export async function getEnvContents(
|
|||||||
extensionConfig,
|
extensionConfig,
|
||||||
extensionId,
|
extensionId,
|
||||||
ExtensionSettingScope.WORKSPACE,
|
ExtensionSettingScope.WORKSPACE,
|
||||||
|
workspaceDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ...userSettings, ...workspaceSettings };
|
return { ...userSettings, ...workspaceSettings };
|
||||||
@@ -196,6 +207,7 @@ export async function updateSetting(
|
|||||||
settingKey: string,
|
settingKey: string,
|
||||||
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
requestSetting: (setting: ExtensionSetting) => Promise<string>,
|
||||||
scope: ExtensionSettingScope,
|
scope: ExtensionSettingScope,
|
||||||
|
workspaceDir?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { name: extensionName, settings } = extensionConfig;
|
const { name: extensionName, settings } = extensionConfig;
|
||||||
if (!settings || settings.length === 0) {
|
if (!settings || settings.length === 0) {
|
||||||
@@ -214,7 +226,7 @@ export async function updateSetting(
|
|||||||
|
|
||||||
const newValue = await requestSetting(settingToUpdate);
|
const newValue = await requestSetting(settingToUpdate);
|
||||||
const keychain = new KeychainTokenStorage(
|
const keychain = new KeychainTokenStorage(
|
||||||
getKeychainStorageName(extensionName, extensionId, scope),
|
getKeychainStorageName(extensionName, extensionId, scope, workspaceDir),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (settingToUpdate.sensitive) {
|
if (settingToUpdate.sensitive) {
|
||||||
@@ -224,7 +236,7 @@ export async function updateSetting(
|
|||||||
|
|
||||||
// For non-sensitive settings, we need to read the existing .env file,
|
// For non-sensitive settings, we need to read the existing .env file,
|
||||||
// update the value, and write it back, preserving any other values.
|
// update the value, and write it back, preserving any other values.
|
||||||
const envFilePath = getEnvFilePath(extensionName, scope);
|
const envFilePath = getEnvFilePath(extensionName, scope, workspaceDir);
|
||||||
let envContent = '';
|
let envContent = '';
|
||||||
if (fsSync.existsSync(envFilePath)) {
|
if (fsSync.existsSync(envFilePath)) {
|
||||||
envContent = await fs.readFile(envFilePath, 'utf-8');
|
envContent = await fs.readFile(envFilePath, 'utf-8');
|
||||||
@@ -302,13 +314,18 @@ async function clearSettings(
|
|||||||
export async function getMissingSettings(
|
export async function getMissingSettings(
|
||||||
extensionConfig: ExtensionConfig,
|
extensionConfig: ExtensionConfig,
|
||||||
extensionId: string,
|
extensionId: string,
|
||||||
|
workspaceDir: string,
|
||||||
): Promise<ExtensionSetting[]> {
|
): Promise<ExtensionSetting[]> {
|
||||||
const { settings } = extensionConfig;
|
const { settings } = extensionConfig;
|
||||||
if (!settings || settings.length === 0) {
|
if (!settings || settings.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingSettings = await getEnvContents(extensionConfig, extensionId);
|
const existingSettings = await getEnvContents(
|
||||||
|
extensionConfig,
|
||||||
|
extensionId,
|
||||||
|
workspaceDir,
|
||||||
|
);
|
||||||
const missingSettings: ExtensionSetting[] = [];
|
const missingSettings: ExtensionSetting[] = [];
|
||||||
|
|
||||||
for (const setting of settings) {
|
for (const setting of settings) {
|
||||||
|
|||||||
@@ -154,7 +154,11 @@ describe('extensionUpdates', () => {
|
|||||||
);
|
);
|
||||||
await userKeychain.setSecret('VAR2', 'val2');
|
await userKeychain.setSecret('VAR2', 'val2');
|
||||||
|
|
||||||
const missing = await getMissingSettings(config, extensionId);
|
const missing = await getMissingSettings(
|
||||||
|
config,
|
||||||
|
extensionId,
|
||||||
|
tempWorkspaceDir,
|
||||||
|
);
|
||||||
expect(missing).toEqual([]);
|
expect(missing).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,7 +170,11 @@ describe('extensionUpdates', () => {
|
|||||||
};
|
};
|
||||||
const extensionId = '12345';
|
const extensionId = '12345';
|
||||||
|
|
||||||
const missing = await getMissingSettings(config, extensionId);
|
const missing = await getMissingSettings(
|
||||||
|
config,
|
||||||
|
extensionId,
|
||||||
|
tempWorkspaceDir,
|
||||||
|
);
|
||||||
expect(missing).toHaveLength(1);
|
expect(missing).toHaveLength(1);
|
||||||
expect(missing[0].name).toBe('s1');
|
expect(missing[0].name).toBe('s1');
|
||||||
});
|
});
|
||||||
@@ -181,7 +189,11 @@ describe('extensionUpdates', () => {
|
|||||||
};
|
};
|
||||||
const extensionId = '12345';
|
const extensionId = '12345';
|
||||||
|
|
||||||
const missing = await getMissingSettings(config, extensionId);
|
const missing = await getMissingSettings(
|
||||||
|
config,
|
||||||
|
extensionId,
|
||||||
|
tempWorkspaceDir,
|
||||||
|
);
|
||||||
expect(missing).toHaveLength(1);
|
expect(missing).toHaveLength(1);
|
||||||
expect(missing[0].name).toBe('s2');
|
expect(missing[0].name).toBe('s2');
|
||||||
});
|
});
|
||||||
@@ -201,7 +213,11 @@ describe('extensionUpdates', () => {
|
|||||||
);
|
);
|
||||||
fs.writeFileSync(workspaceEnvPath, 'VAR1=val1');
|
fs.writeFileSync(workspaceEnvPath, 'VAR1=val1');
|
||||||
|
|
||||||
const missing = await getMissingSettings(config, extensionId);
|
const missing = await getMissingSettings(
|
||||||
|
config,
|
||||||
|
extensionId,
|
||||||
|
tempWorkspaceDir,
|
||||||
|
);
|
||||||
expect(missing).toEqual([]);
|
expect(missing).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -142,6 +142,16 @@ describe('<ExtensionsList />', () => {
|
|||||||
value: '1000',
|
value: '1000',
|
||||||
envVar: 'MAX_TOKENS',
|
envVar: 'MAX_TOKENS',
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
|
scope: 'user' as const,
|
||||||
|
source: '/path/to/.env',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'model',
|
||||||
|
value: 'gemini-pro',
|
||||||
|
envVar: 'MODEL',
|
||||||
|
sensitive: false,
|
||||||
|
scope: 'workspace' as const,
|
||||||
|
source: 'Keychain',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -151,7 +161,8 @@ describe('<ExtensionsList />', () => {
|
|||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain('settings:');
|
expect(output).toContain('settings:');
|
||||||
expect(output).toContain('- sensitiveApiKey: ***');
|
expect(output).toContain('- sensitiveApiKey: ***');
|
||||||
expect(output).toContain('- maxTokens: 1000');
|
expect(output).toContain('- maxTokens: 1000 (User - /path/to/.env)');
|
||||||
|
expect(output).toContain('- model: gemini-pro (Workspace - Keychain)');
|
||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ export const ExtensionsList: React.FC<ExtensionsList> = ({ extensions }) => {
|
|||||||
{ext.resolvedSettings.map((setting) => (
|
{ext.resolvedSettings.map((setting) => (
|
||||||
<Text key={setting.name}>
|
<Text key={setting.name}>
|
||||||
- {setting.name}: {setting.value}
|
- {setting.name}: {setting.value}
|
||||||
|
{setting.scope && (
|
||||||
|
<Text color="gray">
|
||||||
|
{' '}
|
||||||
|
(
|
||||||
|
{setting.scope.charAt(0).toUpperCase() +
|
||||||
|
setting.scope.slice(1)}
|
||||||
|
{setting.source ? ` - ${setting.source}` : ''})
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ export interface ResolvedExtensionSetting {
|
|||||||
envVar: string;
|
envVar: string;
|
||||||
value: string;
|
value: string;
|
||||||
sensitive: boolean;
|
sensitive: boolean;
|
||||||
|
scope?: 'user' | 'workspace';
|
||||||
|
source?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CliHelpAgentSettings {
|
export interface CliHelpAgentSettings {
|
||||||
|
|||||||
Reference in New Issue
Block a user