buroventures-harald-code
Version:
Harald Code - AI-powered coding assistant CLI
339 lines • 12.9 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'fs';
import * as path from 'path';
import { homedir, platform } from 'os';
import * as dotenv from 'dotenv';
import { GEMINI_CONFIG_DIR as GEMINI_DIR, getErrorMessage, } from 'buroventures-harald-code-core';
import stripJsonComments from 'strip-json-comments';
import { DefaultLight } from '../ui/themes/default-light.js';
import { DefaultDark } from '../ui/themes/default.js';
export const SETTINGS_DIRECTORY_NAME = '.qwen';
export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME);
export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
export function getSystemSettingsPath() {
if (process.env.GEMINI_CLI_SYSTEM_SETTINGS_PATH) {
return process.env.GEMINI_CLI_SYSTEM_SETTINGS_PATH;
}
if (platform() === 'darwin') {
return '/Library/Application Support/QwenCode/settings.json';
}
else if (platform() === 'win32') {
return 'C:\\ProgramData\\qwen-code\\settings.json';
}
else {
return '/etc/qwen-code/settings.json';
}
}
export function getWorkspaceSettingsPath(workspaceDir) {
return path.join(workspaceDir, SETTINGS_DIRECTORY_NAME, 'settings.json');
}
export var SettingScope;
(function (SettingScope) {
SettingScope["User"] = "User";
SettingScope["Workspace"] = "Workspace";
SettingScope["System"] = "System";
})(SettingScope || (SettingScope = {}));
export class LoadedSettings {
constructor(system, user, workspace, errors) {
this.system = system;
this.user = user;
this.workspace = workspace;
this.errors = errors;
this._merged = this.computeMergedSettings();
}
system;
user;
workspace;
errors;
_merged;
get merged() {
return this._merged;
}
computeMergedSettings() {
const system = this.system.settings;
const user = this.user.settings;
const workspace = this.workspace.settings;
return {
...user,
...workspace,
...system,
customThemes: {
...(user.customThemes || {}),
...(workspace.customThemes || {}),
...(system.customThemes || {}),
},
mcpServers: {
...(user.mcpServers || {}),
...(workspace.mcpServers || {}),
...(system.mcpServers || {}),
},
};
}
forScope(scope) {
switch (scope) {
case SettingScope.User:
return this.user;
case SettingScope.Workspace:
return this.workspace;
case SettingScope.System:
return this.system;
default:
throw new Error(`Invalid scope: ${scope}`);
}
}
setValue(scope, key, value) {
const settingsFile = this.forScope(scope);
settingsFile.settings[key] = value;
this._merged = this.computeMergedSettings();
saveSettings(settingsFile);
}
}
function resolveEnvVarsInString(value) {
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) {
if (obj === null ||
obj === undefined ||
typeof obj === 'boolean' ||
typeof obj === 'number') {
return obj;
}
if (typeof obj === 'string') {
return resolveEnvVarsInString(obj);
}
if (Array.isArray(obj)) {
return obj.map((item) => resolveEnvVarsInObject(item));
}
if (typeof obj === 'object') {
const newObj = { ...obj };
for (const key in newObj) {
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
newObj[key] = resolveEnvVarsInObject(newObj[key]);
}
}
return newObj;
}
return obj;
}
function findEnvFile(startDir) {
let currentDir = path.resolve(startDir);
while (true) {
// prefer gemini-specific .env under GEMINI_DIR
const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
if (fs.existsSync(geminiEnvPath)) {
return geminiEnvPath;
}
const envPath = path.join(currentDir, '.env');
if (fs.existsSync(envPath)) {
return envPath;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir || !parentDir) {
// check .env under home as fallback, again preferring gemini-specific .env
const homeGeminiEnvPath = path.join(homedir(), GEMINI_DIR, '.env');
if (fs.existsSync(homeGeminiEnvPath)) {
return homeGeminiEnvPath;
}
const homeEnvPath = path.join(homedir(), '.env');
if (fs.existsSync(homeEnvPath)) {
return homeEnvPath;
}
return null;
}
currentDir = parentDir;
}
}
export function setUpCloudShellEnvironment(envFilePath) {
// Special handling for GOOGLE_CLOUD_PROJECT in Cloud Shell:
// Because GOOGLE_CLOUD_PROJECT in Cloud Shell tracks the project
// set by the user using "gcloud config set project" we do not want to
// use its value. So, unless the user overrides GOOGLE_CLOUD_PROJECT in
// one of the .env files, we set the Cloud Shell-specific default here.
if (envFilePath && fs.existsSync(envFilePath)) {
const envFileContent = fs.readFileSync(envFilePath);
const parsedEnv = dotenv.parse(envFileContent);
if (parsedEnv.GOOGLE_CLOUD_PROJECT) {
// .env file takes precedence in Cloud Shell
process.env.GOOGLE_CLOUD_PROJECT = parsedEnv.GOOGLE_CLOUD_PROJECT;
}
else {
// If not in .env, set to default and override global
process.env.GOOGLE_CLOUD_PROJECT = 'cloudshell-gca';
}
}
else {
// If no .env file, set to default and override global
process.env.GOOGLE_CLOUD_PROJECT = 'cloudshell-gca';
}
}
export function loadEnvironment(settings) {
const envFilePath = findEnvFile(process.cwd());
// Cloud Shell environment variable handling
if (process.env.CLOUD_SHELL === 'true') {
setUpCloudShellEnvironment(envFilePath);
}
// If no settings provided, try to load workspace settings for exclusions
let resolvedSettings = settings;
if (!resolvedSettings) {
const workspaceSettingsPath = getWorkspaceSettingsPath(process.cwd());
try {
if (fs.existsSync(workspaceSettingsPath)) {
const workspaceContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
const parsedWorkspaceSettings = JSON.parse(stripJsonComments(workspaceContent));
resolvedSettings = resolveEnvVarsInObject(parsedWorkspaceSettings);
}
}
catch (_e) {
// Ignore errors loading workspace settings
}
}
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);
const excludedVars = resolvedSettings?.excludedProjectEnvVars || DEFAULT_EXCLUDED_ENV_VARS;
const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR);
for (const key in parsedEnv) {
if (Object.hasOwn(parsedEnv, key)) {
// If it's a project .env file, skip loading excluded variables.
if (isProjectEnvFile && excludedVars.includes(key)) {
continue;
}
// Load variable only if it's not already set in the environment.
if (!Object.hasOwn(process.env, key)) {
process.env[key] = parsedEnv[key];
}
}
}
}
catch (_e) {
// Errors are ignored to match the behavior of `dotenv.config({ quiet: true })`.
}
}
}
/**
* Loads settings from user and workspace directories.
* Project settings override user settings.
*/
export function loadSettings(workspaceDir) {
let systemSettings = {};
let userSettings = {};
let workspaceSettings = {};
const settingsErrors = [];
const systemSettingsPath = getSystemSettingsPath();
// FIX: Resolve paths to their canonical representation to handle symlinks
const resolvedWorkspaceDir = path.resolve(workspaceDir);
const resolvedHomeDir = path.resolve(homedir());
let realWorkspaceDir = resolvedWorkspaceDir;
try {
// fs.realpathSync gets the "true" path, resolving any symlinks
realWorkspaceDir = fs.realpathSync(resolvedWorkspaceDir);
}
catch (_e) {
// This is okay. The path might not exist yet, and that's a valid state.
}
// We expect homedir to always exist and be resolvable.
const realHomeDir = fs.realpathSync(resolvedHomeDir);
const workspaceSettingsPath = getWorkspaceSettingsPath(workspaceDir);
// Load system settings
try {
if (fs.existsSync(systemSettingsPath)) {
const systemContent = fs.readFileSync(systemSettingsPath, 'utf-8');
const parsedSystemSettings = JSON.parse(stripJsonComments(systemContent));
systemSettings = resolveEnvVarsInObject(parsedSystemSettings);
}
}
catch (error) {
settingsErrors.push({
message: getErrorMessage(error),
path: systemSettingsPath,
});
}
// Load user settings
try {
if (fs.existsSync(USER_SETTINGS_PATH)) {
const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
const parsedUserSettings = JSON.parse(stripJsonComments(userContent));
userSettings = resolveEnvVarsInObject(parsedUserSettings);
// Support legacy theme names
if (userSettings.theme && userSettings.theme === 'VS') {
userSettings.theme = DefaultLight.name;
}
else if (userSettings.theme && userSettings.theme === 'VS2015') {
userSettings.theme = DefaultDark.name;
}
}
}
catch (error) {
settingsErrors.push({
message: getErrorMessage(error),
path: USER_SETTINGS_PATH,
});
}
// This comparison is now much more reliable.
if (realWorkspaceDir !== realHomeDir) {
// Load workspace settings
try {
if (fs.existsSync(workspaceSettingsPath)) {
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
const parsedWorkspaceSettings = JSON.parse(stripJsonComments(projectContent));
workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings);
if (workspaceSettings.theme && workspaceSettings.theme === 'VS') {
workspaceSettings.theme = DefaultLight.name;
}
else if (workspaceSettings.theme &&
workspaceSettings.theme === 'VS2015') {
workspaceSettings.theme = DefaultDark.name;
}
}
}
catch (error) {
settingsErrors.push({
message: getErrorMessage(error),
path: workspaceSettingsPath,
});
}
}
// Create LoadedSettings first
const loadedSettings = new LoadedSettings({
path: systemSettingsPath,
settings: systemSettings,
}, {
path: USER_SETTINGS_PATH,
settings: userSettings,
}, {
path: workspaceSettingsPath,
settings: workspaceSettings,
}, settingsErrors);
// Load environment with merged settings
loadEnvironment(loadedSettings.merged);
return loadedSettings;
}
export function saveSettings(settingsFile) {
try {
// Ensure the directory exists
const dirPath = path.dirname(settingsFile.path);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
fs.writeFileSync(settingsFile.path, JSON.stringify(settingsFile.settings, null, 2), 'utf-8');
}
catch (error) {
console.error('Error saving user settings file:', error);
}
}
//# sourceMappingURL=settings.js.map