From 4e9d365407564e0f440bf4645607aa47a1d16bca Mon Sep 17 00:00:00 2001 From: jerop Date: Fri, 6 Jun 2025 13:54:59 +0000 Subject: feat: Enable environment variable substitution in settings This commit introduces the ability to use system environment variables within the settings files (e.g., `settings.json`). Users can now reference environment variables using the `${VAR_NAME}` syntax. This enhancement improves security and flexibility, particularly for configurations like MCP server settings, which often require sensitive tokens. Previously, to configure an MCP server, a token might be directly embedded: ```json "mcpServers": { "github": { "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "pat_abc123" } // ... } } ``` With this change, the same configuration can securely reference an environment variable: ```json "mcpServers": { "github": { "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" } // ... } } ``` This allows users to avoid storing secrets directly in configuration files. --- packages/cli/src/config/settings.ts | 41 +++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) (limited to 'packages/cli/src/config/settings.ts') diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index db5dabb6..8205a018 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -98,6 +98,39 @@ export class LoadedSettings { } } +function resolveEnvVarsInString(value: string): string { + const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME} + return value.replace(envVarRegex, (match, varName1, varName2) => { + const varName = varName1 || varName2; + if (process && process.env && typeof process.env[varName] === 'string') { + return process.env[varName]!; + } + return match; + }); +} + +function resolveEnvVarsInObject(obj: T): T { + if (typeof obj === 'string') { + return resolveEnvVarsInString(obj) as unknown as T; + } + + if (Array.isArray(obj)) { + return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T; + } + + if (obj && typeof obj === 'object') { + const newObj = { ...obj } as T; + for (const key in newObj) { + if (Object.prototype.hasOwnProperty.call(newObj, key)) { + newObj[key] = resolveEnvVarsInObject(newObj[key]); + } + } + return newObj; + } + + return obj; +} + /** * Loads settings from user and workspace directories. * Project settings override user settings. @@ -110,7 +143,10 @@ export function loadSettings(workspaceDir: string): LoadedSettings { try { if (fs.existsSync(USER_SETTINGS_PATH)) { const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8'); - userSettings = JSON.parse(stripJsonComments(userContent)) as Settings; + const parsedUserSettings = JSON.parse( + stripJsonComments(userContent), + ) as Settings; + userSettings = resolveEnvVarsInObject(parsedUserSettings); // Support legacy theme names if (userSettings.theme && userSettings.theme === 'VS') { userSettings.theme = DefaultLight.name; @@ -132,9 +168,10 @@ export function loadSettings(workspaceDir: string): LoadedSettings { try { if (fs.existsSync(workspaceSettingsPath)) { const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8'); - workspaceSettings = JSON.parse( + const parsedWorkspaceSettings = JSON.parse( stripJsonComments(projectContent), ) as Settings; + workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings); if (workspaceSettings.theme && workspaceSettings.theme === 'VS') { workspaceSettings.theme = DefaultLight.name; } else if ( -- cgit v1.2.3