2025-07-22 19:59:07 +08:00
/ * *
* @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 ,
2025-07-23 00:27:14 +08:00
} from '@qwen-code/qwen-code-core' ;
2025-07-22 19:59:07 +08:00
import stripJsonComments from 'strip-json-comments' ;
import { DefaultLight } from '../ui/themes/default-light.js' ;
import { DefaultDark } from '../ui/themes/default.js' ;
2025-08-10 09:04:52 +09:00
import { Settings , MemoryImportFormat } from './settingsSchema.js' ;
export type { Settings , MemoryImportFormat } ;
2025-07-22 19:59:07 +08:00
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' ) ;
2025-08-03 20:44:15 +02:00
export const DEFAULT_EXCLUDED_ENV_VARS = [ 'DEBUG' , 'DEBUG_MODE' ] ;
2025-07-22 19:59:07 +08:00
2025-07-21 20:14:07 +00:00
export function getSystemSettingsPath ( ) : string {
if ( process . env . GEMINI_CLI_SYSTEM_SETTINGS_PATH ) {
return process . env . GEMINI_CLI_SYSTEM_SETTINGS_PATH ;
}
2025-07-22 19:59:07 +08:00
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' ;
}
}
2025-08-03 20:44:15 +02:00
export function getWorkspaceSettingsPath ( workspaceDir : string ) : string {
return path . join ( workspaceDir , SETTINGS_DIRECTORY_NAME , 'settings.json' ) ;
}
2025-08-10 09:04:52 +09:00
export type { DnsResolutionOrder } from './settingsSchema.js' ;
2025-08-01 12:30:39 -07:00
2025-07-22 19:59:07 +08:00
export enum SettingScope {
User = 'User' ,
Workspace = 'Workspace' ,
System = 'System' ,
}
export interface CheckpointingSettings {
enabled? : boolean ;
}
2025-07-15 10:22:31 -07:00
export interface SummarizeToolOutputSettings {
tokenBudget? : number ;
}
2025-07-22 19:59:07 +08:00
export interface AccessibilitySettings {
disableLoadingPhrases? : boolean ;
}
export interface SettingsError {
message : string ;
path : string ;
}
export interface SettingsFile {
settings : Settings ;
path : string ;
}
export class LoadedSettings {
constructor (
system : SettingsFile ,
user : SettingsFile ,
workspace : SettingsFile ,
errors : SettingsError [ ] ,
) {
this . system = system ;
this . user = user ;
this . workspace = workspace ;
this . errors = errors ;
this . _merged = this . computeMergedSettings ( ) ;
}
readonly system : SettingsFile ;
readonly user : SettingsFile ;
readonly workspace : SettingsFile ;
readonly errors : SettingsError [ ] ;
private _merged : Settings ;
get merged ( ) : Settings {
return this . _merged ;
}
private computeMergedSettings ( ) : Settings {
2025-07-20 16:51:18 +09:00
const system = this . system . settings ;
const user = this . user . settings ;
const workspace = this . workspace . settings ;
2025-08-07 14:06:17 -07:00
// folderTrust is not supported at workspace level.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { folderTrust , . . . workspaceWithoutFolderTrust } = workspace ;
2025-07-22 19:59:07 +08:00
return {
2025-07-20 16:51:18 +09:00
. . . user ,
2025-08-07 14:06:17 -07:00
. . . workspaceWithoutFolderTrust ,
2025-07-20 16:51:18 +09:00
. . . system ,
customThemes : {
. . . ( user . customThemes || { } ) ,
. . . ( workspace . customThemes || { } ) ,
. . . ( system . customThemes || { } ) ,
} ,
mcpServers : {
. . . ( user . mcpServers || { } ) ,
. . . ( workspace . mcpServers || { } ) ,
. . . ( system . mcpServers || { } ) ,
} ,
2025-08-06 02:01:01 +09:00
includeDirectories : [
. . . ( system . includeDirectories || [ ] ) ,
. . . ( user . includeDirectories || [ ] ) ,
. . . ( workspace . includeDirectories || [ ] ) ,
] ,
2025-08-07 07:34:40 -07:00
chatCompression : {
. . . ( system . chatCompression || { } ) ,
. . . ( user . chatCompression || { } ) ,
. . . ( workspace . chatCompression || { } ) ,
} ,
2025-07-22 19:59:07 +08:00
} ;
}
forScope ( scope : SettingScope ) : SettingsFile {
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 } ` ) ;
}
}
2025-07-20 16:51:18 +09:00
setValue < K extends keyof Settings > (
2025-07-22 19:59:07 +08:00
scope : SettingScope ,
2025-07-20 16:51:18 +09:00
key : K ,
value : Settings [ K ] ,
2025-07-22 19:59:07 +08:00
) : void {
const settingsFile = this . forScope ( scope ) ;
settingsFile . settings [ key ] = value ;
this . _merged = this . computeMergedSettings ( ) ;
saveSettings ( settingsFile ) ;
}
}
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 (
obj === null ||
obj === undefined ||
typeof obj === 'boolean' ||
typeof obj === 'number'
) {
return obj ;
}
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 ( 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 ;
}
function findEnvFile ( startDir : string ) : string | null {
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 : string | null ) : void {
// 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' ;
}
}
2025-08-03 20:44:15 +02:00
export function loadEnvironment ( settings? : Settings ) : void {
2025-07-22 19:59:07 +08:00
const envFilePath = findEnvFile ( process . cwd ( ) ) ;
2025-08-03 20:44:15 +02:00
// Cloud Shell environment variable handling
2025-07-22 19:59:07 +08:00
if ( process . env . CLOUD_SHELL === 'true' ) {
setUpCloudShellEnvironment ( envFilePath ) ;
}
2025-08-03 20:44:15 +02:00
// 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 ) ,
) as Settings ;
resolvedSettings = resolveEnvVarsInObject ( parsedWorkspaceSettings ) ;
}
} catch ( _e ) {
// Ignore errors loading workspace settings
}
}
2025-07-22 19:59:07 +08:00
if ( envFilePath ) {
2025-08-03 20:44:15 +02:00
// 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 })`.
}
2025-07-22 19:59:07 +08:00
}
}
/ * *
* Loads settings from user and workspace directories .
* Project settings override user settings .
* /
export function loadSettings ( workspaceDir : string ) : LoadedSettings {
let systemSettings : Settings = { } ;
let userSettings : Settings = { } ;
let workspaceSettings : Settings = { } ;
const settingsErrors : SettingsError [ ] = [ ] ;
2025-07-21 20:14:07 +00:00
const systemSettingsPath = getSystemSettingsPath ( ) ;
2025-08-02 03:52:17 +05:30
2025-08-05 21:10:16 +02:00
// Resolve paths to their canonical representation to handle symlinks
2025-08-02 03:52:17 +05:30
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 ) ;
2025-08-03 20:44:15 +02:00
const workspaceSettingsPath = getWorkspaceSettingsPath ( workspaceDir ) ;
2025-07-22 19:59:07 +08:00
// Load system settings
try {
2025-07-21 20:14:07 +00:00
if ( fs . existsSync ( systemSettingsPath ) ) {
const systemContent = fs . readFileSync ( systemSettingsPath , 'utf-8' ) ;
2025-07-22 19:59:07 +08:00
const parsedSystemSettings = JSON . parse (
stripJsonComments ( systemContent ) ,
) as Settings ;
systemSettings = resolveEnvVarsInObject ( parsedSystemSettings ) ;
}
} catch ( error : unknown ) {
settingsErrors . push ( {
message : getErrorMessage ( error ) ,
2025-07-21 20:14:07 +00:00
path : systemSettingsPath ,
2025-07-22 19:59:07 +08:00
} ) ;
}
// 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 ) ,
) as Settings ;
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 : unknown ) {
settingsErrors . push ( {
message : getErrorMessage ( error ) ,
path : USER_SETTINGS_PATH ,
} ) ;
}
2025-08-02 03:52:17 +05:30
if ( realWorkspaceDir !== realHomeDir ) {
// Load workspace settings
try {
if ( fs . existsSync ( workspaceSettingsPath ) ) {
const projectContent = fs . readFileSync ( workspaceSettingsPath , 'utf-8' ) ;
const parsedWorkspaceSettings = JSON . parse (
stripJsonComments ( projectContent ) ,
) as Settings ;
workspaceSettings = resolveEnvVarsInObject ( parsedWorkspaceSettings ) ;
if ( workspaceSettings . theme && workspaceSettings . theme === 'VS' ) {
workspaceSettings . theme = DefaultLight . name ;
} else if (
workspaceSettings . theme &&
workspaceSettings . theme === 'VS2015'
) {
workspaceSettings . theme = DefaultDark . name ;
}
2025-07-22 19:59:07 +08:00
}
2025-08-02 03:52:17 +05:30
} catch ( error : unknown ) {
settingsErrors . push ( {
message : getErrorMessage ( error ) ,
path : workspaceSettingsPath ,
} ) ;
2025-07-22 19:59:07 +08:00
}
}
2025-08-03 20:44:15 +02:00
// Create LoadedSettings first
const loadedSettings = new LoadedSettings (
2025-07-22 19:59:07 +08:00
{
2025-07-21 20:14:07 +00:00
path : systemSettingsPath ,
2025-07-22 19:59:07 +08:00
settings : systemSettings ,
} ,
{
path : USER_SETTINGS_PATH ,
settings : userSettings ,
} ,
{
path : workspaceSettingsPath ,
settings : workspaceSettings ,
} ,
settingsErrors ,
) ;
2025-08-03 20:44:15 +02:00
2025-08-07 07:34:40 -07:00
// Validate chatCompression settings
const chatCompression = loadedSettings . merged . chatCompression ;
const threshold = chatCompression ? . contextPercentageThreshold ;
if (
threshold != null &&
( typeof threshold !== 'number' || threshold < 0 || threshold > 1 )
) {
console . warn (
` Invalid value for chatCompression.contextPercentageThreshold: " ${ threshold } ". Please use a value between 0 and 1. Using default compression settings. ` ,
) ;
delete loadedSettings . merged . chatCompression ;
}
2025-08-03 20:44:15 +02:00
// Load environment with merged settings
loadEnvironment ( loadedSettings . merged ) ;
return loadedSettings ;
2025-07-22 19:59:07 +08:00
}
export function saveSettings ( settingsFile : SettingsFile ) : void {
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 ) ;
}
}