summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/config/settings.test.ts160
-rw-r--r--packages/cli/src/config/settings.ts41
2 files changed, 199 insertions, 2 deletions
diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts
index c6943dbe..350c5d33 100644
--- a/packages/cli/src/config/settings.test.ts
+++ b/packages/cli/src/config/settings.test.ts
@@ -322,6 +322,166 @@ describe('Settings Loading and Merging', () => {
consoleErrorSpy.mockRestore();
});
+
+ it('should resolve environment variables in user settings', () => {
+ process.env.TEST_API_KEY = 'user_api_key_from_env';
+ const userSettingsContent = {
+ apiKey: '$TEST_API_KEY',
+ someUrl: 'https://test.com/${TEST_API_KEY}',
+ };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.user.settings.apiKey).toBe('user_api_key_from_env');
+ expect(settings.user.settings.someUrl).toBe(
+ 'https://test.com/user_api_key_from_env',
+ );
+ expect(settings.merged.apiKey).toBe('user_api_key_from_env');
+ delete process.env.TEST_API_KEY;
+ });
+
+ it('should resolve environment variables in workspace settings', () => {
+ process.env.WORKSPACE_ENDPOINT = 'workspace_endpoint_from_env';
+ const workspaceSettingsContent = {
+ endpoint: '${WORKSPACE_ENDPOINT}/api',
+ nested: { value: '$WORKSPACE_ENDPOINT' },
+ };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
+ return JSON.stringify(workspaceSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.workspace.settings.endpoint).toBe(
+ 'workspace_endpoint_from_env/api',
+ );
+ expect(settings.workspace.settings.nested.value).toBe(
+ 'workspace_endpoint_from_env',
+ );
+ expect(settings.merged.endpoint).toBe('workspace_endpoint_from_env/api');
+ delete process.env.WORKSPACE_ENDPOINT;
+ });
+
+ it('should prioritize workspace env variables over user env variables if keys clash after resolution', () => {
+ const userSettingsContent = { configValue: '$SHARED_VAR' };
+ const workspaceSettingsContent = { configValue: '$SHARED_VAR' };
+
+ (mockFsExistsSync as Mock).mockReturnValue(true);
+ const originalSharedVar = process.env.SHARED_VAR;
+ // Temporarily delete to ensure a clean slate for the test's specific manipulations
+ delete process.env.SHARED_VAR;
+
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH) {
+ process.env.SHARED_VAR = 'user_value_for_user_read'; // Set for user settings read
+ return JSON.stringify(userSettingsContent);
+ }
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
+ process.env.SHARED_VAR = 'workspace_value_for_workspace_read'; // Set for workspace settings read
+ return JSON.stringify(workspaceSettingsContent);
+ }
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+
+ expect(settings.user.settings.configValue).toBe(
+ 'user_value_for_user_read',
+ );
+ expect(settings.workspace.settings.configValue).toBe(
+ 'workspace_value_for_workspace_read',
+ );
+ // Merged should take workspace's resolved value
+ expect(settings.merged.configValue).toBe(
+ 'workspace_value_for_workspace_read',
+ );
+
+ // Restore original environment variable state
+ if (originalSharedVar !== undefined) {
+ process.env.SHARED_VAR = originalSharedVar;
+ } else {
+ delete process.env.SHARED_VAR; // Ensure it's deleted if it wasn't there before
+ }
+ });
+
+ it('should leave unresolved environment variables as is', () => {
+ const userSettingsContent = { apiKey: '$UNDEFINED_VAR' };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.user.settings.apiKey).toBe('$UNDEFINED_VAR');
+ expect(settings.merged.apiKey).toBe('$UNDEFINED_VAR');
+ });
+
+ it('should resolve multiple environment variables in a single string', () => {
+ process.env.VAR_A = 'valueA';
+ process.env.VAR_B = 'valueB';
+ const userSettingsContent = { path: '/path/$VAR_A/${VAR_B}/end' };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.user.settings.path).toBe('/path/valueA/valueB/end');
+ delete process.env.VAR_A;
+ delete process.env.VAR_B;
+ });
+
+ it('should resolve environment variables in arrays', () => {
+ process.env.ITEM_1 = 'item1_env';
+ process.env.ITEM_2 = 'item2_env';
+ const userSettingsContent = { list: ['$ITEM_1', '${ITEM_2}', 'literal'] };
+ (mockFsExistsSync as Mock).mockImplementation(
+ (p: fs.PathLike) => p === USER_SETTINGS_PATH,
+ );
+ (fs.readFileSync as Mock).mockImplementation(
+ (p: fs.PathOrFileDescriptor) => {
+ if (p === USER_SETTINGS_PATH)
+ return JSON.stringify(userSettingsContent);
+ return '{}';
+ },
+ );
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
+ expect(settings.user.settings.list).toEqual([
+ 'item1_env',
+ 'item2_env',
+ 'literal',
+ ]);
+ delete process.env.ITEM_1;
+ delete process.env.ITEM_2;
+ });
});
describe('LoadedSettings class', () => {
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<T>(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 (