Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions packages/a2a-server/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';
import * as dotenv from 'dotenv';

import type { TelemetryTarget } from '@google/gemini-cli-core';
Expand All @@ -17,9 +16,9 @@ import {
FileDiscoveryService,
ApprovalMode,
loadServerHierarchicalMemory,
GEMINI_DIR,
DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_GEMINI_MODEL,
Storage,
type ExtensionLoader,
} from '@google/gemini-cli-core';

Expand Down Expand Up @@ -156,7 +155,7 @@ 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');
const geminiEnvPath = path.join(currentDir, '.env');
if (fs.existsSync(geminiEnvPath)) {
return geminiEnvPath;
}
Expand All @@ -167,11 +166,11 @@ function findEnvFile(startDir: string): string | null {
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir || !parentDir) {
// check .env under home as fallback, again preferring gemini-specific .env
const homeGeminiEnvPath = path.join(process.cwd(), GEMINI_DIR, '.env');
const homeGeminiEnvPath = path.join(Storage.getConfigDir(), '.env');
if (fs.existsSync(homeGeminiEnvPath)) {
return homeGeminiEnvPath;
}
const homeEnvPath = path.join(homedir(), '.env');
const homeEnvPath = path.join(Storage.getConfigDir(), '.env');
if (fs.existsSync(homeEnvPath)) {
return homeEnvPath;
}
Expand Down
7 changes: 3 additions & 4 deletions packages/a2a-server/src/config/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@
// Copied exactly from packages/cli/src/config/extension.ts, last PR #1026

import {
GEMINI_DIR,
Storage,
type MCPServerConfig,
type ExtensionInstallMetadata,
type GeminiCLIExtension,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { logger } from '../utils/logger.js';

export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
export const EXTENSIONS_DIRECTORY_NAME = 'extensions';
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';

Expand All @@ -39,7 +38,7 @@ interface ExtensionConfig {
export function loadExtensions(workspaceDir: string): GeminiCLIExtension[] {
const allExtensions = [
...loadExtensionsFromDir(workspaceDir),
...loadExtensionsFromDir(os.homedir()),
...loadExtensionsFromDir(Storage.getConfigDir()),
];

const uniqueExtensions: GeminiCLIExtension[] = [];
Expand Down
21 changes: 16 additions & 5 deletions packages/a2a-server/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@

import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';

import type { MCPServerConfig } from '@google/gemini-cli-core';
import {
debugLogger,
GEMINI_DIR,
getErrorMessage,
Storage,
type TelemetrySettings,
} from '@google/gemini-cli-core';
import stripJsonComments from 'strip-json-comments';

export const USER_SETTINGS_DIR = path.join(homedir(), GEMINI_DIR);
export const USER_SETTINGS_DIR = Storage.getConfigDir();
export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
export const WORKSPACE_SETTINGS_DIR = Storage.getWorkspaceGeminiDir();

// Reconcile with https://github.com/google-gemini/gemini-cli/blob/b09bc6656080d4d12e1d06734aae2ec33af5c1ed/packages/cli/src/config/settings.ts#L53
export interface Settings {
Expand Down Expand Up @@ -61,6 +61,18 @@ export function loadSettings(workspaceDir: string): Settings {

// Load user settings
try {
if (fs.existsSync(workspaceDir || WORKSPACE_SETTINGS_DIR)) {
const workspaceSettingsPath = path.join(
workspaceDir || WORKSPACE_SETTINGS_DIR,
'settings.json',
);
const userContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
const parsedUserSettings = JSON.parse(
stripJsonComments(userContent),
) as Settings;
userSettings = resolveEnvVarsInObject(parsedUserSettings);
}

if (fs.existsSync(USER_SETTINGS_PATH)) {
const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8');
const parsedUserSettings = JSON.parse(
Expand All @@ -76,8 +88,7 @@ export function loadSettings(workspaceDir: string): Settings {
}

const workspaceSettingsPath = path.join(
workspaceDir,
GEMINI_DIR,
Storage.getWorkspaceGeminiDir(),
'settings.json',
);

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/config/extensions/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class ExtensionStorage {
}

static getUserExtensionsDir(): string {
return new Storage(os.homedir()).getExtensionsDir();
return path.join(Storage.getConfigDir(), 'extensions');
}

static async createTmpDir(): Promise<string> {
Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/config/extensions/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as path from 'node:path';
import { type VariableSchema, VARIABLE_SCHEMA } from './variableSchema.js';
import { GEMINI_DIR } from '@google/gemini-cli-core';

export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
export const EXTENSIONS_DIRECTORY_NAME = 'extensions';
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
export const EXTENSION_SETTINGS_FILENAME = '.env';
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,9 +505,9 @@ function findEnvFile(startDir: string): string | null {
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 userGeminiEnvPath = path.join(Storage.getConfigDir(), '.env');
if (fs.existsSync(userGeminiEnvPath)) {
return userGeminiEnvPath;
}
const homeEnvPath = path.join(homedir(), '.env');
if (fs.existsSync(homeEnvPath)) {
Expand Down
9 changes: 2 additions & 7 deletions packages/cli/src/config/trustedFolders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,23 @@

import * as fs from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';
import {
FatalConfigError,
getErrorMessage,
isWithinRoot,
ideContextStore,
GEMINI_DIR,
Storage,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
import stripJsonComments from 'strip-json-comments';

export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json';

export function getUserSettingsDir(): string {
return path.join(homedir(), GEMINI_DIR);
}

export function getTrustedFoldersPath(): string {
if (process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']) {
return process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH'];
}
return path.join(getUserSettingsDir(), TRUSTED_FOLDERS_FILENAME);
return path.join(Storage.getConfigDir(), TRUSTED_FOLDERS_FILENAME);
}

export enum TrustLevel {
Expand Down
5 changes: 2 additions & 3 deletions packages/cli/src/ui/components/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import { theme } from '../semantic-colors.js';
import { StreamingState } from '../types.js';
import { UpdateNotification } from './UpdateNotification.js';

import { GEMINI_DIR, Storage } from '@google/gemini-cli-core';
import { Storage } from '@google/gemini-cli-core';

import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

const settingsPath = path.join(os.homedir(), GEMINI_DIR, 'settings.json');
const settingsPath = path.join(Storage.getConfigDir(), 'settings.json');

const screenReaderNudgeFilePath = path.join(
Storage.getGlobalTempDir(),
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/src/ui/hooks/useExtensionUpdates.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as os from 'node:os';
import * as path from 'node:path';
import { createExtension } from '../../test-utils/createExtension.js';
import { useExtensionUpdates } from './useExtensionUpdates.js';
import { GEMINI_DIR } from '@google/gemini-cli-core';
import { Storage } from '@google/gemini-cli-core';
import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { MessageType } from '../types.js';
Expand All @@ -22,6 +22,17 @@ import { ExtensionUpdateState } from '../state/extensions.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings } from '../../config/settings.js';

beforeEach(() => {
vi.stubEnv('XDG_CONFIG_HOME', '');
vi.stubEnv('XDG_CACHE_HOME', '');
vi.stubEnv('XDG_DATA_HOME', '');
vi.stubEnv('XDG_STATE_HOME', '');
});

afterEach(() => {
vi.unstubAllEnvs();
});

vi.mock('os', async (importOriginal) => {
const mockedOs = await importOriginal<typeof os>();
return {
Expand Down Expand Up @@ -50,7 +61,7 @@ describe('useExtensionUpdates', () => {
path.join(tempHomeDir, 'gemini-cli-test-workspace-'),
);
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
userExtensionsDir = path.join(tempHomeDir, GEMINI_DIR, 'extensions');
userExtensionsDir = path.join(Storage.getConfigDir(), 'extensions');
fs.mkdirSync(userExtensionsDir, { recursive: true });
vi.mocked(checkForAllExtensionUpdates).mockReset();
vi.mocked(updateExtension).mockReset();
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/utils/persistentState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { PersistentState } from './persistentState.js';
vi.mock('node:fs');
vi.mock('@google/gemini-cli-core', () => ({
Storage: {
getGlobalGeminiDir: vi.fn(),
getStateDir: vi.fn(),
},
debugLogger: {
warn: vi.fn(),
Expand All @@ -27,7 +27,7 @@ describe('PersistentState', () => {

beforeEach(() => {
vi.resetAllMocks();
vi.mocked(Storage.getGlobalGeminiDir).mockReturnValue(mockDir);
vi.mocked(Storage.getStateDir).mockReturnValue(mockDir);
persistentState = new PersistentState();
});

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/utils/persistentState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class PersistentState {

private getPath(): string {
if (!this.filePath) {
this.filePath = path.join(Storage.getGlobalGeminiDir(), STATE_FILENAME);
this.filePath = path.join(Storage.getStateDir(), STATE_FILENAME);
}
return this.filePath;
}
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/code_assist/oauth-credential-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import { HybridTokenStorage } from '../mcp/token-storage/hybrid-token-storage.js
import { OAUTH_FILE } from '../config/storage.js';
import type { OAuthCredentials } from '../mcp/token-storage/types.js';
import * as path from 'node:path';
import * as os from 'node:os';
import { promises as fs } from 'node:fs';
import { GEMINI_DIR } from '../utils/paths.js';
import { Storage } from '../config/storage.js';
import { coreEvents } from '../utils/events.js';

const KEYCHAIN_SERVICE_NAME = 'gemini-cli-oauth';
Expand Down Expand Up @@ -91,7 +90,7 @@ export class OAuthCredentialStorage {
await this.storage.deleteCredentials(MAIN_ACCOUNT_KEY);

// Also try to remove the old file if it exists
const oldFilePath = path.join(os.homedir(), GEMINI_DIR, OAUTH_FILE);
const oldFilePath = Storage.getOAuthCredsPath();
await fs.rm(oldFilePath, { force: true }).catch(() => {});
} catch (error: unknown) {
coreEvents.emitFeedback(
Expand All @@ -107,7 +106,7 @@ export class OAuthCredentialStorage {
* Migrate credentials from old file-based storage to keychain
*/
private static async migrateFromFileStorage(): Promise<Credentials | null> {
const oldFilePath = path.join(os.homedir(), GEMINI_DIR, OAUTH_FILE);
const oldFilePath = path.join(Storage.getOAuthCredsPath(), OAUTH_FILE);

let credsJson: string;
try {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/code_assist/oauth2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ describe('oauth2', () => {
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
);
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
fs.mkdirSync(path.join(tempHomeDir, GEMINI_DIR), { recursive: true });
});
afterEach(() => {
fs.rmSync(tempHomeDir, { recursive: true, force: true });
Expand Down
43 changes: 21 additions & 22 deletions packages/core/src/config/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,32 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, vi } from 'vitest';
import * as os from 'node:os';
import { describe, it, expect } from 'vitest';
import * as path from 'node:path';

vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
return {
...actual,
mkdirSync: vi.fn(),
};
});

import * as os from 'node:os';
import { Storage } from './storage.js';
import { GEMINI_DIR } from '../utils/paths.js';

describe('Storage – getGlobalSettingsPath', () => {
it('returns path to ~/.gemini/settings.json', () => {
const expected = path.join(os.homedir(), GEMINI_DIR, 'settings.json');
const homedir = os.homedir();
const MOCK_GLOBAL_GEMINI_DIR = path.join(homedir, GEMINI_DIR);

describe('Storage – getGlobalSettingsPath no XDG', () => {
it('returns path in homedir', () => {
const expected = MOCK_GLOBAL_GEMINI_DIR;
expect(Storage.getCacheDir()).toBe(expected);
expect(Storage.getConfigDir()).toBe(expected);
expect(Storage.getDataDir()).toBe(expected);
expect(Storage.getStateDir()).toBe(expected);
});

it('returns path to $HOME/gemini/settings.json', () => {
const expected = path.join(MOCK_GLOBAL_GEMINI_DIR, 'settings.json');
expect(Storage.getGlobalSettingsPath()).toBe(expected);
});
});

describe('Storage – additional helpers', () => {
describe('Storage – additional helpers, no xdg env var', () => {
const projectRoot = '/tmp/project';
const storage = new Storage(projectRoot);

Expand All @@ -36,7 +39,7 @@ describe('Storage – additional helpers', () => {
});

it('getUserCommandsDir returns ~/.gemini/commands', () => {
const expected = path.join(os.homedir(), GEMINI_DIR, 'commands');
const expected = path.join(MOCK_GLOBAL_GEMINI_DIR, 'commands');
expect(Storage.getUserCommandsDir()).toBe(expected);
});

Expand All @@ -46,16 +49,12 @@ describe('Storage – additional helpers', () => {
});

it('getMcpOAuthTokensPath returns ~/.gemini/mcp-oauth-tokens.json', () => {
const expected = path.join(
os.homedir(),
GEMINI_DIR,
'mcp-oauth-tokens.json',
);
const expected = path.join(MOCK_GLOBAL_GEMINI_DIR, 'mcp-oauth-tokens.json');
expect(Storage.getMcpOAuthTokensPath()).toBe(expected);
});

it('getGlobalBinDir returns ~/.gemini/tmp/bin', () => {
const expected = path.join(os.homedir(), GEMINI_DIR, 'tmp', 'bin');
it('getGlobalBinDir returns ~/.gemini/bin', () => {
const expected = path.join(MOCK_GLOBAL_GEMINI_DIR, 'bin');
expect(Storage.getGlobalBinDir()).toBe(expected);
});
});
Loading