mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 12:40:44 +00:00
feat(settings): add settings.env field for environment variable configuration
Add support for defining environment variables in settings.json files. These variables are loaded with the lowest priority: 1. System/process environment variables (highest) 2. .env files 3. settings.env (lowest/fallback) This allows users to configure default environment variables in their user or workspace settings without conflicting with existing env vars or .env file values. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
36931e1eab
commit
5f8884b60c
3 changed files with 224 additions and 4 deletions
|
|
@ -2691,6 +2691,194 @@ describe('Settings Loading and Merging', () => {
|
|||
|
||||
expect(process.env['TESTTEST']).not.toEqual('1234');
|
||||
});
|
||||
|
||||
describe('settings.env field', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env['ENV_FROM_SETTINGS'];
|
||||
delete process.env['ENV_OVERRIDE_TEST'];
|
||||
delete process.env['SYSTEM_ENV_VAR'];
|
||||
delete process.env['MULTI_VAR_A'];
|
||||
delete process.env['MULTI_VAR_B'];
|
||||
delete process.env['MULTI_VAR_C'];
|
||||
delete process.env['USER_ENV_VAR'];
|
||||
delete process.env['WORKSPACE_ENV_VAR'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it('should load environment variables from settings.env as fallback', () => {
|
||||
const userSettingsContent: Settings = {
|
||||
env: {
|
||||
ENV_FROM_SETTINGS: 'settings_value',
|
||||
},
|
||||
};
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) =>
|
||||
[USER_SETTINGS_PATH].includes(p.toString()),
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
|
||||
// loadSettings internally calls loadEnvironment with userSettings
|
||||
loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(process.env['ENV_FROM_SETTINGS']).toEqual('settings_value');
|
||||
});
|
||||
|
||||
it('should allow .env file to override settings.env values', () => {
|
||||
const geminiEnvPath = path.resolve(path.join(QWEN_DIR, '.env'));
|
||||
const userSettingsContent: Settings = {
|
||||
env: {
|
||||
ENV_OVERRIDE_TEST: 'from_settings',
|
||||
},
|
||||
};
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) =>
|
||||
[USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()),
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === geminiEnvPath) return 'ENV_OVERRIDE_TEST=from_dotenv';
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
|
||||
// loadSettings internally calls loadEnvironment with merged settings
|
||||
loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// .env file has higher priority than settings.env (loaded first, no-override)
|
||||
expect(process.env['ENV_OVERRIDE_TEST']).toEqual('from_dotenv');
|
||||
});
|
||||
|
||||
it('should not override existing system environment variables', () => {
|
||||
process.env['SYSTEM_ENV_VAR'] = 'system_value';
|
||||
|
||||
const geminiEnvPath = path.resolve(path.join(QWEN_DIR, '.env'));
|
||||
const userSettingsContent: Settings = {
|
||||
env: {
|
||||
SYSTEM_ENV_VAR: 'from_settings',
|
||||
},
|
||||
};
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) =>
|
||||
[USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()),
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === geminiEnvPath) return 'SYSTEM_ENV_VAR=from_dotenv';
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
|
||||
// loadSettings internally calls loadEnvironment with userSettings
|
||||
loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// System environment variable should have highest priority
|
||||
expect(process.env['SYSTEM_ENV_VAR']).toEqual('system_value');
|
||||
});
|
||||
|
||||
it('should support multiple env variables in settings.env', () => {
|
||||
const userSettingsContent: Settings = {
|
||||
env: {
|
||||
MULTI_VAR_A: 'value_a',
|
||||
MULTI_VAR_B: 'value_b',
|
||||
MULTI_VAR_C: 'value_c',
|
||||
},
|
||||
};
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) =>
|
||||
[USER_SETTINGS_PATH].includes(p.toString()),
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
|
||||
// loadSettings internally calls loadEnvironment with userSettings
|
||||
loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
expect(process.env['MULTI_VAR_A']).toEqual('value_a');
|
||||
expect(process.env['MULTI_VAR_B']).toEqual('value_b');
|
||||
expect(process.env['MULTI_VAR_C']).toEqual('value_c');
|
||||
});
|
||||
|
||||
it('should load settings.env from both user and workspace settings', () => {
|
||||
const workspaceSettingsContent = {
|
||||
env: {
|
||||
WORKSPACE_ENV_VAR: 'workspace_value',
|
||||
},
|
||||
};
|
||||
const userSettingsContent: Settings = {
|
||||
env: {
|
||||
USER_ENV_VAR: 'user_value',
|
||||
},
|
||||
};
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) =>
|
||||
[USER_SETTINGS_PATH, MOCK_WORKSPACE_SETTINGS_PATH].includes(
|
||||
p.toString(),
|
||||
),
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
|
||||
// loadSettings internally calls loadEnvironment with merged settings
|
||||
loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Both user-level and workspace-level env should be loaded
|
||||
expect(process.env['USER_ENV_VAR']).toEqual('user_value');
|
||||
expect(process.env['WORKSPACE_ENV_VAR']).toEqual('workspace_value');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('needsMigration', () => {
|
||||
|
|
|
|||
|
|
@ -848,7 +848,16 @@ export function setUpCloudShellEnvironment(envFilePath: string | null): void {
|
|||
process.env['GOOGLE_CLOUD_PROJECT'] = 'cloudshell-gca';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads environment variables from .env files and settings.env.
|
||||
*
|
||||
* Priority order (highest to lowest):
|
||||
* 1. CLI flags
|
||||
* 2. process.env (system/export/inline environment variables)
|
||||
* 3. .env files (no-override mode)
|
||||
* 4. settings.env (no-override mode)
|
||||
* 5. defaults
|
||||
*/
|
||||
export function loadEnvironment(settings: Settings): void {
|
||||
const envFilePath = findEnvFile(process.cwd());
|
||||
|
||||
|
|
@ -861,9 +870,9 @@ export function loadEnvironment(settings: Settings): void {
|
|||
setUpCloudShellEnvironment(envFilePath);
|
||||
}
|
||||
|
||||
// Step 1: Load from .env files (higher priority than settings.env)
|
||||
// Only set if not already present in process.env (no-override mode)
|
||||
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);
|
||||
|
|
@ -879,7 +888,7 @@ export function loadEnvironment(settings: Settings): void {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Load variable only if it's not already set in the environment.
|
||||
// Only set if not already present in process.env (no-override)
|
||||
if (!Object.hasOwn(process.env, key)) {
|
||||
process.env[key] = parsedEnv[key];
|
||||
}
|
||||
|
|
@ -889,6 +898,16 @@ export function loadEnvironment(settings: Settings): void {
|
|||
// Errors are ignored to match the behavior of `dotenv.config({ quiet: true })`.
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Load environment variables from settings.env as fallback (lowest priority)
|
||||
// Only set if not already present (no-override, after .env is loaded)
|
||||
if (settings.env) {
|
||||
for (const [key, value] of Object.entries(settings.env)) {
|
||||
if (!Object.hasOwn(process.env, key) && typeof value === 'string') {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -116,6 +116,19 @@ const SETTINGS_SCHEMA = {
|
|||
mergeStrategy: MergeStrategy.REPLACE,
|
||||
},
|
||||
|
||||
// Environment variables fallback
|
||||
env: {
|
||||
type: 'object',
|
||||
label: 'Environment Variables',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: {} as Record<string, string>,
|
||||
description:
|
||||
'Environment variables to set as fallback defaults. These are loaded with the lowest priority: system environment variables > .env files > settings.env.',
|
||||
showInDialog: false,
|
||||
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
|
||||
},
|
||||
|
||||
general: {
|
||||
type: 'object',
|
||||
label: 'General',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue