chore: Extract '.gemini' to GEMINI_DIR constant (#10540)

Co-authored-by: Richie Foreman <richie.foreman@gmail.com>
This commit is contained in:
Dongin Kim(Terry)
2025-10-14 02:31:39 +09:00
committed by GitHub
parent 7beaa368a9
commit 518caae62e
36 changed files with 181 additions and 157 deletions
+2 -11
View File
@@ -19,23 +19,14 @@ import {
} from 'node:fs/promises'; } from 'node:fs/promises';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import * as os from 'node:os'; import { getGlobalMemoryFilePath } from '../packages/core/src/tools/memoryTool.js';
import {
GEMINI_CONFIG_DIR,
DEFAULT_CONTEXT_FILENAME,
} from '../packages/core/src/tools/memoryTool.js';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..'); const rootDir = join(__dirname, '..');
const integrationTestsDir = join(rootDir, '.integration-tests'); const integrationTestsDir = join(rootDir, '.integration-tests');
let runDir = ''; // Make runDir accessible in teardown let runDir = ''; // Make runDir accessible in teardown
const memoryFilePath = join( const memoryFilePath = getGlobalMemoryFilePath();
os.homedir(),
GEMINI_CONFIG_DIR,
DEFAULT_CONTEXT_FILENAME,
);
let originalMemoryContent: string | null = null; let originalMemoryContent: string | null = null;
export async function setup() { export async function setup() {
+2 -1
View File
@@ -15,6 +15,7 @@ import fs from 'node:fs';
import * as pty from '@lydell/node-pty'; import * as pty from '@lydell/node-pty';
import stripAnsi from 'strip-ansi'; import stripAnsi from 'strip-ansi';
import * as os from 'node:os'; import * as os from 'node:os';
import { GEMINI_DIR } from '../packages/core/src/utils/paths.js';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -244,7 +245,7 @@ export class TestRig {
mkdirSync(this.testDir, { recursive: true }); mkdirSync(this.testDir, { recursive: true });
// Create a settings file to point the CLI to the local collector // Create a settings file to point the CLI to the local collector
const geminiDir = join(this.testDir, '.gemini'); const geminiDir = join(this.testDir, GEMINI_DIR);
mkdirSync(geminiDir, { recursive: true }); mkdirSync(geminiDir, { recursive: true });
// In sandbox mode, use an absolute path for telemetry inside the container // In sandbox mode, use an absolute path for telemetry inside the container
// The container mounts the test directory at the same path as the host // The container mounts the test directory at the same path as the host
+3 -7
View File
@@ -17,7 +17,7 @@ import {
FileDiscoveryService, FileDiscoveryService,
ApprovalMode, ApprovalMode,
loadServerHierarchicalMemory, loadServerHierarchicalMemory,
GEMINI_CONFIG_DIR, GEMINI_DIR,
DEFAULT_GEMINI_EMBEDDING_MODEL, DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_MODEL,
type GeminiCLIExtension, type GeminiCLIExtension,
@@ -176,7 +176,7 @@ function findEnvFile(startDir: string): string | null {
let currentDir = path.resolve(startDir); let currentDir = path.resolve(startDir);
while (true) { while (true) {
// prefer gemini-specific .env under GEMINI_DIR // prefer gemini-specific .env under GEMINI_DIR
const geminiEnvPath = path.join(currentDir, GEMINI_CONFIG_DIR, '.env'); const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env');
if (fs.existsSync(geminiEnvPath)) { if (fs.existsSync(geminiEnvPath)) {
return geminiEnvPath; return geminiEnvPath;
} }
@@ -187,11 +187,7 @@ function findEnvFile(startDir: string): string | null {
const parentDir = path.dirname(currentDir); const parentDir = path.dirname(currentDir);
if (parentDir === currentDir || !parentDir) { if (parentDir === currentDir || !parentDir) {
// check .env under home as fallback, again preferring gemini-specific .env // check .env under home as fallback, again preferring gemini-specific .env
const homeGeminiEnvPath = path.join( const homeGeminiEnvPath = path.join(process.cwd(), GEMINI_DIR, '.env');
process.cwd(),
GEMINI_CONFIG_DIR,
'.env',
);
if (fs.existsSync(homeGeminiEnvPath)) { if (fs.existsSync(homeGeminiEnvPath)) {
return homeGeminiEnvPath; return homeGeminiEnvPath;
} }
+6 -5
View File
@@ -6,17 +6,18 @@
// Copied exactly from packages/cli/src/config/extension.ts, last PR #1026 // Copied exactly from packages/cli/src/config/extension.ts, last PR #1026
import type { import {
MCPServerConfig, GEMINI_DIR,
ExtensionInstallMetadata, type MCPServerConfig,
GeminiCLIExtension, type ExtensionInstallMetadata,
type GeminiCLIExtension,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import * as os from 'node:os'; import * as os from 'node:os';
import { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions'); export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json'; export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json'; export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
+3 -3
View File
@@ -10,13 +10,13 @@ import { homedir } from 'node:os';
import type { MCPServerConfig } from '@google/gemini-cli-core'; import type { MCPServerConfig } from '@google/gemini-cli-core';
import { import {
GEMINI_DIR,
getErrorMessage, getErrorMessage,
type TelemetrySettings, type TelemetrySettings,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import stripJsonComments from 'strip-json-comments'; import stripJsonComments from 'strip-json-comments';
export const SETTINGS_DIRECTORY_NAME = '.gemini'; export const USER_SETTINGS_DIR = path.join(homedir(), GEMINI_DIR);
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 USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
// Reconcile with https://github.com/google-gemini/gemini-cli/blob/b09bc6656080d4d12e1d06734aae2ec33af5c1ed/packages/cli/src/config/settings.ts#L53 // Reconcile with https://github.com/google-gemini/gemini-cli/blob/b09bc6656080d4d12e1d06734aae2ec33af5c1ed/packages/cli/src/config/settings.ts#L53
@@ -76,7 +76,7 @@ export function loadSettings(workspaceDir: string): Settings {
const workspaceSettingsPath = path.join( const workspaceSettingsPath = path.join(
workspaceDir, workspaceDir,
SETTINGS_DIRECTORY_NAME, GEMINI_DIR,
'settings.json', 'settings.json',
); );
+1 -1
View File
@@ -32,7 +32,7 @@ vi.mock('@google/gemini-cli-core', () => ({
getWorkspaceSettingsPath: () => '/tmp/gemini/workspace-settings.json', getWorkspaceSettingsPath: () => '/tmp/gemini/workspace-settings.json',
getProjectTempDir: () => '/test/home/.gemini/tmp/mocked_hash', getProjectTempDir: () => '/test/home/.gemini/tmp/mocked_hash',
})), })),
GEMINI_CONFIG_DIR: '.gemini', GEMINI_DIR: '.gemini',
getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)),
})); }));
vi.mock('@modelcontextprotocol/sdk/client/index.js'); vi.mock('@modelcontextprotocol/sdk/client/index.js');
+2 -1
View File
@@ -11,6 +11,7 @@ import { removeCommand } from './remove.js';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import * as os from 'node:os'; import * as os from 'node:os';
import { GEMINI_DIR } from '@google/gemini-cli-core';
vi.mock('fs/promises', () => ({ vi.mock('fs/promises', () => ({
readFile: vi.fn(), readFile: vi.fn(),
@@ -80,7 +81,7 @@ describe('mcp remove command', () => {
vi.restoreAllMocks(); vi.restoreAllMocks();
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-remove-test-')); tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-remove-test-'));
settingsDir = path.join(tempDir, '.gemini'); settingsDir = path.join(tempDir, GEMINI_DIR);
settingsPath = path.join(settingsDir, 'settings.json'); settingsPath = path.join(settingsDir, 'settings.json');
fs.mkdirSync(settingsDir, { recursive: true }); fs.mkdirSync(settingsDir, { recursive: true });
+4 -1
View File
@@ -1171,7 +1171,10 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
// 3. Spies on console functions (for logger output) are correctly set up if needed. // 3. Spies on console functions (for logger output) are correctly set up if needed.
// Example of a previously failing test structure: // Example of a previously failing test structure:
it.skip('should correctly use mocked homedir for global path', async () => { it.skip('should correctly use mocked homedir for global path', async () => {
const MOCK_GEMINI_DIR_LOCAL = path.join('/mock/home/user', '.gemini'); const MOCK_GEMINI_DIR_LOCAL = path.join(
'/mock/home/user',
ServerConfig.GEMINI_DIR,
);
const MOCK_GLOBAL_PATH_LOCAL = path.join( const MOCK_GLOBAL_PATH_LOCAL = path.join(
MOCK_GEMINI_DIR_LOCAL, MOCK_GEMINI_DIR_LOCAL,
'GEMINI.md', 'GEMINI.md',
@@ -9,7 +9,8 @@ import fs from 'node:fs';
import os from 'node:os'; import os from 'node:os';
import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { ExtensionEnablementManager, Override } from './extensionEnablement.js'; import { ExtensionEnablementManager, Override } from './extensionEnablement.js';
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
import { GEMINI_DIR, type GeminiCLIExtension } from '@google/gemini-cli-core';
// Helper to create a temporary directory for testing // Helper to create a temporary directory for testing
function createTestDir() { function createTestDir() {
@@ -27,7 +28,7 @@ let manager: ExtensionEnablementManager;
describe('ExtensionEnablementManager', () => { describe('ExtensionEnablementManager', () => {
beforeEach(() => { beforeEach(() => {
testDir = createTestDir(); testDir = createTestDir();
configDir = path.join(testDir.path, '.gemini'); configDir = path.join(testDir.path, GEMINI_DIR);
manager = new ExtensionEnablementManager(configDir); manager = new ExtensionEnablementManager(configDir);
}); });
+2 -3
View File
@@ -59,7 +59,6 @@ import {
USER_SETTINGS_PATH, // This IS the mocked path. USER_SETTINGS_PATH, // This IS the mocked path.
getSystemSettingsPath, getSystemSettingsPath,
getSystemDefaultsPath, getSystemDefaultsPath,
SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock.
migrateSettingsToV1, migrateSettingsToV1,
needsMigration, needsMigration,
type Settings, type Settings,
@@ -70,10 +69,10 @@ import {
import { FatalConfigError, GEMINI_DIR } from '@google/gemini-cli-core'; import { FatalConfigError, GEMINI_DIR } from '@google/gemini-cli-core';
const MOCK_WORKSPACE_DIR = '/mock/workspace'; const MOCK_WORKSPACE_DIR = '/mock/workspace';
// Use the (mocked) SETTINGS_DIRECTORY_NAME for consistency // Use the (mocked) GEMINI_DIR for consistency
const MOCK_WORKSPACE_SETTINGS_PATH = pathActual.join( const MOCK_WORKSPACE_SETTINGS_PATH = pathActual.join(
MOCK_WORKSPACE_DIR, MOCK_WORKSPACE_DIR,
SETTINGS_DIRECTORY_NAME, GEMINI_DIR,
'settings.json', 'settings.json',
); );
+1 -3
View File
@@ -11,7 +11,7 @@ import * as dotenv from 'dotenv';
import process from 'node:process'; import process from 'node:process';
import { import {
FatalConfigError, FatalConfigError,
GEMINI_CONFIG_DIR as GEMINI_DIR, GEMINI_DIR,
getErrorMessage, getErrorMessage,
Storage, Storage,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
@@ -49,8 +49,6 @@ function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined {
export type { Settings, MemoryImportFormat }; export type { Settings, MemoryImportFormat };
export const SETTINGS_DIRECTORY_NAME = '.gemini';
export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath(); export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath();
export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH); export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH);
export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE']; export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
+2 -2
View File
@@ -12,13 +12,13 @@ import {
getErrorMessage, getErrorMessage,
isWithinRoot, isWithinRoot,
ideContextStore, ideContextStore,
GEMINI_DIR,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { Settings } from './settings.js'; import type { Settings } from './settings.js';
import stripJsonComments from 'strip-json-comments'; import stripJsonComments from 'strip-json-comments';
export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json'; export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json';
export const SETTINGS_DIRECTORY_NAME = '.gemini'; export const USER_SETTINGS_DIR = path.join(homedir(), GEMINI_DIR);
export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME);
export function getTrustedFoldersPath(): string { export function getTrustedFoldersPath(): string {
if (process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']) { if (process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']) {
@@ -6,7 +6,7 @@
import * as path from 'node:path'; import * as path from 'node:path';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import { Storage } from '@google/gemini-cli-core'; import { GEMINI_DIR, Storage } from '@google/gemini-cli-core';
import mock from 'mock-fs'; import mock from 'mock-fs';
import { FileCommandLoader } from './FileCommandLoader.js'; import { FileCommandLoader } from './FileCommandLoader.js';
import { assert, vi } from 'vitest'; import { assert, vi } from 'vitest';
@@ -529,7 +529,9 @@ describe('FileCommandLoader', () => {
).getProjectCommandsDir(); ).getProjectCommandsDir();
const extensionDir = path.join( const extensionDir = path.join(
process.cwd(), process.cwd(),
'.gemini/extensions/test-ext', GEMINI_DIR,
'extensions',
'test-ext',
); );
mock({ mock({
@@ -582,7 +584,9 @@ describe('FileCommandLoader', () => {
).getProjectCommandsDir(); ).getProjectCommandsDir();
const extensionDir = path.join( const extensionDir = path.join(
process.cwd(), process.cwd(),
'.gemini/extensions/test-ext', GEMINI_DIR,
'extensions',
'test-ext',
); );
mock({ mock({
@@ -678,11 +682,15 @@ describe('FileCommandLoader', () => {
it('only loads commands from active extensions', async () => { it('only loads commands from active extensions', async () => {
const extensionDir1 = path.join( const extensionDir1 = path.join(
process.cwd(), process.cwd(),
'.gemini/extensions/active-ext', GEMINI_DIR,
'extensions',
'active-ext',
); );
const extensionDir2 = path.join( const extensionDir2 = path.join(
process.cwd(), process.cwd(),
'.gemini/extensions/inactive-ext', GEMINI_DIR,
'extensions',
'inactive-ext',
); );
mock({ mock({
@@ -737,7 +745,9 @@ describe('FileCommandLoader', () => {
it('handles missing extension commands directory gracefully', async () => { it('handles missing extension commands directory gracefully', async () => {
const extensionDir = path.join( const extensionDir = path.join(
process.cwd(), process.cwd(),
'.gemini/extensions/no-commands', GEMINI_DIR,
'extensions',
'no-commands',
); );
mock({ mock({
@@ -769,7 +779,12 @@ describe('FileCommandLoader', () => {
}); });
it('handles nested command structure in extensions', async () => { it('handles nested command structure in extensions', async () => {
const extensionDir = path.join(process.cwd(), '.gemini/extensions/a'); const extensionDir = path.join(
process.cwd(),
GEMINI_DIR,
'extensions',
'a',
);
mock({ mock({
[extensionDir]: { [extensionDir]: {
@@ -11,7 +11,11 @@ import * as path from 'node:path';
import { restoreCommand } from './restoreCommand.js'; import { restoreCommand } from './restoreCommand.js';
import { type CommandContext } from './types.js'; import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import type { Config, GitService } from '@google/gemini-cli-core'; import {
GEMINI_DIR,
type Config,
type GitService,
} from '@google/gemini-cli-core';
describe('restoreCommand', () => { describe('restoreCommand', () => {
let mockContext: CommandContext; let mockContext: CommandContext;
@@ -26,7 +30,7 @@ describe('restoreCommand', () => {
testRootDir = await fs.mkdtemp( testRootDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'restore-command-test-'), path.join(os.tmpdir(), 'restore-command-test-'),
); );
geminiTempDir = path.join(testRootDir, '.gemini'); geminiTempDir = path.join(testRootDir, GEMINI_DIR);
checkpointsDir = path.join(geminiTempDir, 'checkpoints'); checkpointsDir = path.join(geminiTempDir, 'checkpoints');
// The command itself creates this, but for tests it's easier to have it ready. // The command itself creates this, but for tests it's easier to have it ready.
// Some tests might remove it to test error paths. // Some tests might remove it to test error paths.
@@ -11,10 +11,11 @@ import { theme } from '../semantic-colors.js';
import { StreamingState } from '../types.js'; import { StreamingState } from '../types.js';
import { UpdateNotification } from './UpdateNotification.js'; import { UpdateNotification } from './UpdateNotification.js';
import { GEMINI_DIR } from '@google/gemini-cli-core';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import path from 'node:path'; import path from 'node:path';
const settingsPath = path.join(homedir(), '.gemini', 'settings.json'); const settingsPath = path.join(homedir(), GEMINI_DIR, 'settings.json');
export const Notifications = () => { export const Notifications = () => {
const { startupWarnings } = useAppContext(); const { startupWarnings } = useAppContext();
@@ -19,6 +19,7 @@ vi.mock('../contexts/ConfigContext.js');
vi.mock('../contexts/UIStateContext.js'); vi.mock('../contexts/UIStateContext.js');
vi.mock('@google/gemini-cli-core', () => ({ vi.mock('@google/gemini-cli-core', () => ({
recordFlickerFrame: vi.fn(), recordFlickerFrame: vi.fn(),
GEMINI_DIR: '.gemini',
})); }));
vi.mock('ink', async (importOriginal) => { vi.mock('ink', async (importOriginal) => {
const original = await importOriginal<typeof import('ink')>(); const original = await importOriginal<typeof import('ink')>();
@@ -10,30 +10,34 @@ import * as fs from 'node:fs/promises';
import * as path from 'node:path'; import * as path from 'node:path';
import * as os from 'node:os'; import * as os from 'node:os';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import { GEMINI_DIR } from '@google/gemini-cli-core';
vi.mock('fs/promises', () => ({ vi.mock('node:fs/promises', () => ({
readFile: vi.fn(), readFile: vi.fn(),
writeFile: vi.fn(), writeFile: vi.fn(),
mkdir: vi.fn(), mkdir: vi.fn(),
})); }));
vi.mock('os'); vi.mock('node:os');
vi.mock('crypto'); vi.mock('node:crypto');
vi.mock('fs', async (importOriginal) => { vi.mock('node:fs', async (importOriginal) => {
const actualFs = await importOriginal<typeof import('fs')>(); const actualFs = await importOriginal<typeof import('node:fs')>();
return { return {
...actualFs, ...actualFs,
mkdirSync: vi.fn(), mkdirSync: vi.fn(),
}; };
}); });
vi.mock('@google/gemini-cli-core', () => { vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
const path = await import('node:path');
class Storage { class Storage {
getProjectTempDir(): string { getProjectTempDir(): string {
return path.join('/test/home/', '.gemini', 'tmp', 'mocked_hash'); return path.join('/test/home/', actual.GEMINI_DIR, 'tmp', 'mocked_hash');
} }
getHistoryFilePath(): string { getHistoryFilePath(): string {
return path.join( return path.join(
'/test/home/', '/test/home/',
'.gemini', actual.GEMINI_DIR,
'tmp', 'tmp',
'mocked_hash', 'mocked_hash',
'shell_history', 'shell_history',
@@ -41,6 +45,7 @@ vi.mock('@google/gemini-cli-core', () => {
} }
} }
return { return {
...actual,
isNodeError: (err: unknown): err is NodeJS.ErrnoException => isNodeError: (err: unknown): err is NodeJS.ErrnoException =>
typeof err === 'object' && err !== null && 'code' in err, typeof err === 'object' && err !== null && 'code' in err,
Storage, Storage,
@@ -53,7 +58,7 @@ const MOCKED_PROJECT_HASH = 'mocked_hash';
const MOCKED_HISTORY_DIR = path.join( const MOCKED_HISTORY_DIR = path.join(
MOCKED_HOME_DIR, MOCKED_HOME_DIR,
'.gemini', GEMINI_DIR,
'tmp', 'tmp',
MOCKED_PROJECT_HASH, MOCKED_PROJECT_HASH,
); );
+8 -20
View File
@@ -11,13 +11,10 @@ import fs from 'node:fs';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { quote, parse } from 'shell-quote'; import { quote, parse } from 'shell-quote';
import { import { USER_SETTINGS_DIR } from '../config/settings.js';
USER_SETTINGS_DIR,
SETTINGS_DIRECTORY_NAME,
} from '../config/settings.js';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import type { Config, SandboxConfig } from '@google/gemini-cli-core'; import type { Config, SandboxConfig } from '@google/gemini-cli-core';
import { FatalSandboxError } from '@google/gemini-cli-core'; import { FatalSandboxError, GEMINI_DIR } from '@google/gemini-cli-core';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js'; import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
@@ -156,10 +153,7 @@ function entrypoint(workdir: string, cliArgs: string[]): string[] {
shellCmds.push(`export PYTHONPATH="$PYTHONPATH${pythonPathSuffix}";`); shellCmds.push(`export PYTHONPATH="$PYTHONPATH${pythonPathSuffix}";`);
} }
const projectSandboxBashrc = path.join( const projectSandboxBashrc = path.join(GEMINI_DIR, 'sandbox.bashrc');
SETTINGS_DIRECTORY_NAME,
'sandbox.bashrc',
);
if (fs.existsSync(projectSandboxBashrc)) { if (fs.existsSync(projectSandboxBashrc)) {
shellCmds.push(`source ${getContainerPath(projectSandboxBashrc)};`); shellCmds.push(`source ${getContainerPath(projectSandboxBashrc)};`);
} }
@@ -211,10 +205,7 @@ export async function start_sandbox(
); );
// if profile name is not recognized, then look for file under project settings directory // if profile name is not recognized, then look for file under project settings directory
if (!BUILTIN_SEATBELT_PROFILES.includes(profile)) { if (!BUILTIN_SEATBELT_PROFILES.includes(profile)) {
profileFile = path.join( profileFile = path.join(GEMINI_DIR, `sandbox-macos-${profile}.sb`);
SETTINGS_DIRECTORY_NAME,
`sandbox-macos-${profile}.sb`,
);
} }
if (!fs.existsSync(profileFile)) { if (!fs.existsSync(profileFile)) {
throw new FatalSandboxError( throw new FatalSandboxError(
@@ -359,7 +350,7 @@ export async function start_sandbox(
const gcPath = fs.realpathSync(process.argv[1]); const gcPath = fs.realpathSync(process.argv[1]);
const projectSandboxDockerfile = path.join( const projectSandboxDockerfile = path.join(
SETTINGS_DIRECTORY_NAME, GEMINI_DIR,
'sandbox.Dockerfile', 'sandbox.Dockerfile',
); );
const isCustomProjectSandbox = fs.existsSync(projectSandboxDockerfile); const isCustomProjectSandbox = fs.existsSync(projectSandboxDockerfile);
@@ -383,7 +374,7 @@ export async function start_sandbox(
// if project folder has sandbox.Dockerfile under project settings folder, use that // if project folder has sandbox.Dockerfile under project settings folder, use that
let buildArgs = ''; let buildArgs = '';
const projectSandboxDockerfile = path.join( const projectSandboxDockerfile = path.join(
SETTINGS_DIRECTORY_NAME, GEMINI_DIR,
'sandbox.Dockerfile', 'sandbox.Dockerfile',
); );
if (isCustomProjectSandbox) { if (isCustomProjectSandbox) {
@@ -441,7 +432,7 @@ export async function start_sandbox(
// note user/home changes inside sandbox and we mount at BOTH paths for consistency // note user/home changes inside sandbox and we mount at BOTH paths for consistency
const userSettingsDirOnHost = USER_SETTINGS_DIR; const userSettingsDirOnHost = USER_SETTINGS_DIR;
const userSettingsDirInSandbox = getContainerPath( const userSettingsDirInSandbox = getContainerPath(
`/home/node/${SETTINGS_DIRECTORY_NAME}`, `/home/node/${GEMINI_DIR}`,
); );
if (!fs.existsSync(userSettingsDirOnHost)) { if (!fs.existsSync(userSettingsDirOnHost)) {
fs.mkdirSync(userSettingsDirOnHost); fs.mkdirSync(userSettingsDirOnHost);
@@ -665,10 +656,7 @@ export async function start_sandbox(
?.toLowerCase() ?.toLowerCase()
.startsWith(workdir.toLowerCase()) .startsWith(workdir.toLowerCase())
) { ) {
const sandboxVenvPath = path.resolve( const sandboxVenvPath = path.resolve(GEMINI_DIR, 'sandbox.venv');
SETTINGS_DIRECTORY_NAME,
'sandbox.venv',
);
if (!fs.existsSync(sandboxVenvPath)) { if (!fs.existsSync(sandboxVenvPath)) {
fs.mkdirSync(sandboxVenvPath, { recursive: true }); fs.mkdirSync(sandboxVenvPath, { recursive: true });
} }
@@ -11,8 +11,8 @@ import type { OAuthCredentials } from '../mcp/token-storage/types.js';
import * as path from 'node:path'; import * as path from 'node:path';
import * as os from 'node:os'; import * as os from 'node:os';
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import { GEMINI_DIR } from '../utils/paths.js';
const GEMINI_DIR = '.gemini';
const KEYCHAIN_SERVICE_NAME = 'gemini-cli-oauth'; const KEYCHAIN_SERVICE_NAME = 'gemini-cli-oauth';
const MAIN_ACCOUNT_KEY = 'main-account'; const MAIN_ACCOUNT_KEY = 'main-account';
+38 -13
View File
@@ -25,6 +25,7 @@ import { AuthType } from '../core/contentGenerator.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import readline from 'node:readline'; import readline from 'node:readline';
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js'; import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
import { GEMINI_DIR } from '../utils/paths.js';
vi.mock('os', async (importOriginal) => { vi.mock('os', async (importOriginal) => {
const os = await importOriginal<typeof import('os')>(); const os = await importOriginal<typeof import('os')>();
@@ -182,7 +183,7 @@ describe('oauth2', () => {
// Verify Google Account was cached // Verify Google Account was cached
const googleAccountPath = path.join( const googleAccountPath = path.join(
tempHomeDir, tempHomeDir,
'.gemini', GEMINI_DIR,
'google_accounts.json', 'google_accounts.json',
); );
expect(fs.existsSync(googleAccountPath)).toBe(true); expect(fs.existsSync(googleAccountPath)).toBe(true);
@@ -290,7 +291,11 @@ describe('oauth2', () => {
it('should attempt to load cached credentials first', async () => { it('should attempt to load cached credentials first', async () => {
const cachedCreds = { refresh_token: 'cached-token' }; const cachedCreds = { refresh_token: 'cached-token' };
const credsPath = path.join(tempHomeDir, '.gemini', 'oauth_creds.json'); const credsPath = path.join(
tempHomeDir,
GEMINI_DIR,
'oauth_creds.json',
);
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds)); await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));
@@ -328,7 +333,11 @@ describe('oauth2', () => {
await getOauthClient(AuthType.CLOUD_SHELL, mockConfig); await getOauthClient(AuthType.CLOUD_SHELL, mockConfig);
const credsPath = path.join(tempHomeDir, '.gemini', 'oauth_creds.json'); const credsPath = path.join(
tempHomeDir,
GEMINI_DIR,
'oauth_creds.json',
);
expect(fs.existsSync(credsPath)).toBe(false); expect(fs.existsSync(credsPath)).toBe(false);
}); });
@@ -355,7 +364,7 @@ describe('oauth2', () => {
const defaultCreds = { refresh_token: 'default-cached-token' }; const defaultCreds = { refresh_token: 'default-cached-token' };
const defaultCredsPath = path.join( const defaultCredsPath = path.join(
tempHomeDir, tempHomeDir,
'.gemini', GEMINI_DIR,
'oauth_creds.json', 'oauth_creds.json',
); );
await fs.promises.mkdir(path.dirname(defaultCredsPath), { await fs.promises.mkdir(path.dirname(defaultCredsPath), {
@@ -463,7 +472,7 @@ describe('oauth2', () => {
// Verify Google Account was cached // Verify Google Account was cached
const googleAccountPath = path.join( const googleAccountPath = path.join(
tempHomeDir, tempHomeDir,
'.gemini', GEMINI_DIR,
'google_accounts.json', 'google_accounts.json',
); );
const cachedContent = fs.readFileSync(googleAccountPath, 'utf-8'); const cachedContent = fs.readFileSync(googleAccountPath, 'utf-8');
@@ -493,7 +502,11 @@ describe('oauth2', () => {
// Make it fall through to cached credentials path // Make it fall through to cached credentials path
const cachedCreds = { refresh_token: 'cached-token' }; const cachedCreds = { refresh_token: 'cached-token' };
const credsPath = path.join(tempHomeDir, '.gemini', 'oauth_creds.json'); const credsPath = path.join(
tempHomeDir,
GEMINI_DIR,
'oauth_creds.json',
);
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds)); await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));
@@ -524,7 +537,11 @@ describe('oauth2', () => {
// Make it fall through to cached credentials path // Make it fall through to cached credentials path
const cachedCreds = { refresh_token: 'cached-token' }; const cachedCreds = { refresh_token: 'cached-token' };
const credsPath = path.join(tempHomeDir, '.gemini', 'oauth_creds.json'); const credsPath = path.join(
tempHomeDir,
GEMINI_DIR,
'oauth_creds.json',
);
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds)); await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));
@@ -916,13 +933,17 @@ describe('oauth2', () => {
describe('clearCachedCredentialFile', () => { describe('clearCachedCredentialFile', () => {
it('should clear cached credentials and Google account', async () => { it('should clear cached credentials and Google account', async () => {
const cachedCreds = { refresh_token: 'test-token' }; const cachedCreds = { refresh_token: 'test-token' };
const credsPath = path.join(tempHomeDir, '.gemini', 'oauth_creds.json'); const credsPath = path.join(
tempHomeDir,
GEMINI_DIR,
'oauth_creds.json',
);
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds)); await fs.promises.writeFile(credsPath, JSON.stringify(cachedCreds));
const googleAccountPath = path.join( const googleAccountPath = path.join(
tempHomeDir, tempHomeDir,
'.gemini', GEMINI_DIR,
'google_accounts.json', 'google_accounts.json',
); );
const accountData = { active: 'test@example.com', old: [] }; const accountData = { active: 'test@example.com', old: [] };
@@ -965,7 +986,11 @@ describe('oauth2', () => {
); );
// Pre-populate credentials to make getOauthClient resolve quickly // Pre-populate credentials to make getOauthClient resolve quickly
const credsPath = path.join(tempHomeDir, '.gemini', 'oauth_creds.json'); const credsPath = path.join(
tempHomeDir,
GEMINI_DIR,
'oauth_creds.json',
);
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
await fs.promises.writeFile( await fs.promises.writeFile(
credsPath, credsPath,
@@ -1104,7 +1129,7 @@ describe('oauth2', () => {
expect( expect(
OAuthCredentialStorage.saveCredentials as Mock, OAuthCredentialStorage.saveCredentials as Mock,
).toHaveBeenCalledWith(mockTokens); ).toHaveBeenCalledWith(mockTokens);
const credsPath = path.join(tempHomeDir, '.gemini', 'oauth_creds.json'); const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');
expect(fs.existsSync(credsPath)).toBe(false); expect(fs.existsSync(credsPath)).toBe(false);
}); });
@@ -1120,7 +1145,7 @@ describe('oauth2', () => {
// Create a dummy unencrypted credential file. // Create a dummy unencrypted credential file.
// If the logic is correct, this file should be ignored. // If the logic is correct, this file should be ignored.
const unencryptedCreds = { refresh_token: 'unencrypted-token' }; const unencryptedCreds = { refresh_token: 'unencrypted-token' };
const credsPath = path.join(tempHomeDir, '.gemini', 'oauth_creds.json'); const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
await fs.promises.writeFile(credsPath, JSON.stringify(unencryptedCreds)); await fs.promises.writeFile(credsPath, JSON.stringify(unencryptedCreds));
@@ -1150,7 +1175,7 @@ describe('oauth2', () => {
); );
// Create a dummy unencrypted credential file. It should not be deleted. // Create a dummy unencrypted credential file. It should not be deleted.
const credsPath = path.join(tempHomeDir, '.gemini', 'oauth_creds.json'); const credsPath = path.join(tempHomeDir, GEMINI_DIR, 'oauth_creds.json');
await fs.promises.mkdir(path.dirname(credsPath), { recursive: true }); await fs.promises.mkdir(path.dirname(credsPath), { recursive: true });
await fs.promises.writeFile(credsPath, '{}'); await fs.promises.writeFile(credsPath, '{}');
+1 -1
View File
@@ -81,7 +81,7 @@ vi.mock('../tools/memoryTool', () => ({
setGeminiMdFilename: vi.fn(), setGeminiMdFilename: vi.fn(),
getCurrentGeminiMdFilename: vi.fn(() => 'GEMINI.md'), // Mock the original filename getCurrentGeminiMdFilename: vi.fn(() => 'GEMINI.md'), // Mock the original filename
DEFAULT_CONTEXT_FILENAME: 'GEMINI.md', DEFAULT_CONTEXT_FILENAME: 'GEMINI.md',
GEMINI_CONFIG_DIR: '.gemini', GEMINI_DIR: '.gemini',
})); }));
vi.mock('../core/contentGenerator.js'); vi.mock('../core/contentGenerator.js');
+7 -6
View File
@@ -17,10 +17,11 @@ vi.mock('fs', async (importOriginal) => {
}); });
import { Storage } from './storage.js'; import { Storage } from './storage.js';
import { GEMINI_DIR } from '../utils/paths.js';
describe('Storage getGlobalSettingsPath', () => { describe('Storage getGlobalSettingsPath', () => {
it('returns path to ~/.gemini/settings.json', () => { it('returns path to ~/.gemini/settings.json', () => {
const expected = path.join(os.homedir(), '.gemini', 'settings.json'); const expected = path.join(os.homedir(), GEMINI_DIR, 'settings.json');
expect(Storage.getGlobalSettingsPath()).toBe(expected); expect(Storage.getGlobalSettingsPath()).toBe(expected);
}); });
}); });
@@ -30,31 +31,31 @@ describe('Storage additional helpers', () => {
const storage = new Storage(projectRoot); const storage = new Storage(projectRoot);
it('getWorkspaceSettingsPath returns project/.gemini/settings.json', () => { it('getWorkspaceSettingsPath returns project/.gemini/settings.json', () => {
const expected = path.join(projectRoot, '.gemini', 'settings.json'); const expected = path.join(projectRoot, GEMINI_DIR, 'settings.json');
expect(storage.getWorkspaceSettingsPath()).toBe(expected); expect(storage.getWorkspaceSettingsPath()).toBe(expected);
}); });
it('getUserCommandsDir returns ~/.gemini/commands', () => { it('getUserCommandsDir returns ~/.gemini/commands', () => {
const expected = path.join(os.homedir(), '.gemini', 'commands'); const expected = path.join(os.homedir(), GEMINI_DIR, 'commands');
expect(Storage.getUserCommandsDir()).toBe(expected); expect(Storage.getUserCommandsDir()).toBe(expected);
}); });
it('getProjectCommandsDir returns project/.gemini/commands', () => { it('getProjectCommandsDir returns project/.gemini/commands', () => {
const expected = path.join(projectRoot, '.gemini', 'commands'); const expected = path.join(projectRoot, GEMINI_DIR, 'commands');
expect(storage.getProjectCommandsDir()).toBe(expected); expect(storage.getProjectCommandsDir()).toBe(expected);
}); });
it('getMcpOAuthTokensPath returns ~/.gemini/mcp-oauth-tokens.json', () => { it('getMcpOAuthTokensPath returns ~/.gemini/mcp-oauth-tokens.json', () => {
const expected = path.join( const expected = path.join(
os.homedir(), os.homedir(),
'.gemini', GEMINI_DIR,
'mcp-oauth-tokens.json', 'mcp-oauth-tokens.json',
); );
expect(Storage.getMcpOAuthTokensPath()).toBe(expected); expect(Storage.getMcpOAuthTokensPath()).toBe(expected);
}); });
it('getGlobalBinDir returns ~/.gemini/tmp/bin', () => { it('getGlobalBinDir returns ~/.gemini/tmp/bin', () => {
const expected = path.join(os.homedir(), '.gemini', 'tmp', 'bin'); const expected = path.join(os.homedir(), GEMINI_DIR, 'tmp', 'bin');
expect(Storage.getGlobalBinDir()).toBe(expected); expect(Storage.getGlobalBinDir()).toBe(expected);
}); });
}); });
+2 -2
View File
@@ -8,8 +8,8 @@ import * as path from 'node:path';
import * as os from 'node:os'; import * as os from 'node:os';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { GEMINI_DIR } from '../utils/paths.js';
export const GEMINI_DIR = '.gemini';
export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json'; export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json';
export const OAUTH_FILE = 'oauth_creds.json'; export const OAUTH_FILE = 'oauth_creds.json';
const TMP_DIR_NAME = 'tmp'; const TMP_DIR_NAME = 'tmp';
@@ -25,7 +25,7 @@ export class Storage {
static getGlobalGeminiDir(): string { static getGlobalGeminiDir(): string {
const homeDir = os.homedir(); const homeDir = os.homedir();
if (!homeDir) { if (!homeDir) {
return path.join(os.tmpdir(), '.gemini'); return path.join(os.tmpdir(), GEMINI_DIR);
} }
return path.join(homeDir, GEMINI_DIR); return path.join(homeDir, GEMINI_DIR);
} }
+2 -7
View File
@@ -27,20 +27,15 @@ import type { Content } from '@google/genai';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import os from 'node:os'; import os from 'node:os';
import { GEMINI_DIR } from '../utils/paths.js';
const GEMINI_DIR_NAME = '.gemini';
const TMP_DIR_NAME = 'tmp'; const TMP_DIR_NAME = 'tmp';
const LOG_FILE_NAME = 'logs.json'; const LOG_FILE_NAME = 'logs.json';
const CHECKPOINT_FILE_NAME = 'checkpoint.json'; const CHECKPOINT_FILE_NAME = 'checkpoint.json';
const projectDir = process.cwd(); const projectDir = process.cwd();
const hash = crypto.createHash('sha256').update(projectDir).digest('hex'); const hash = crypto.createHash('sha256').update(projectDir).digest('hex');
const TEST_GEMINI_DIR = path.join( const TEST_GEMINI_DIR = path.join(os.homedir(), GEMINI_DIR, TMP_DIR_NAME, hash);
os.homedir(),
GEMINI_DIR_NAME,
TMP_DIR_NAME,
hash,
);
const TEST_LOG_FILE_PATH = path.join(TEST_GEMINI_DIR, LOG_FILE_NAME); const TEST_LOG_FILE_PATH = path.join(TEST_GEMINI_DIR, LOG_FILE_NAME);
const TEST_CHECKPOINT_FILE_PATH = path.join( const TEST_CHECKPOINT_FILE_PATH = path.join(
+5 -13
View File
@@ -10,9 +10,9 @@ import { isGitRepository } from '../utils/gitUtils.js';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { GEMINI_CONFIG_DIR } from '../tools/memoryTool.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { CodebaseInvestigatorAgent } from '../agents/codebase-investigator.js'; import { CodebaseInvestigatorAgent } from '../agents/codebase-investigator.js';
import { GEMINI_DIR } from '../utils/paths.js';
// Mock tool names if they are dynamically generated or complex // Mock tool names if they are dynamically generated or complex
vi.mock('../tools/ls', () => ({ LSTool: { Name: 'list_directory' } })); vi.mock('../tools/ls', () => ({ LSTool: { Name: 'list_directory' } }));
@@ -223,9 +223,7 @@ describe('Core System Prompt (prompts.ts)', () => {
}); });
it('should read from default path when GEMINI_SYSTEM_MD is "true"', () => { it('should read from default path when GEMINI_SYSTEM_MD is "true"', () => {
const defaultPath = path.resolve( const defaultPath = path.resolve(path.join(GEMINI_DIR, 'system.md'));
path.join(GEMINI_CONFIG_DIR, 'system.md'),
);
vi.stubEnv('GEMINI_SYSTEM_MD', 'true'); vi.stubEnv('GEMINI_SYSTEM_MD', 'true');
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('custom system prompt'); vi.mocked(fs.readFileSync).mockReturnValue('custom system prompt');
@@ -236,9 +234,7 @@ describe('Core System Prompt (prompts.ts)', () => {
}); });
it('should read from default path when GEMINI_SYSTEM_MD is "1"', () => { it('should read from default path when GEMINI_SYSTEM_MD is "1"', () => {
const defaultPath = path.resolve( const defaultPath = path.resolve(path.join(GEMINI_DIR, 'system.md'));
path.join(GEMINI_CONFIG_DIR, 'system.md'),
);
vi.stubEnv('GEMINI_SYSTEM_MD', '1'); vi.stubEnv('GEMINI_SYSTEM_MD', '1');
vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('custom system prompt'); vi.mocked(fs.readFileSync).mockReturnValue('custom system prompt');
@@ -291,9 +287,7 @@ describe('Core System Prompt (prompts.ts)', () => {
}); });
it('should write to default path when GEMINI_WRITE_SYSTEM_MD is "true"', () => { it('should write to default path when GEMINI_WRITE_SYSTEM_MD is "true"', () => {
const defaultPath = path.resolve( const defaultPath = path.resolve(path.join(GEMINI_DIR, 'system.md'));
path.join(GEMINI_CONFIG_DIR, 'system.md'),
);
vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', 'true'); vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', 'true');
getCoreSystemPrompt(mockConfig); getCoreSystemPrompt(mockConfig);
expect(fs.writeFileSync).toHaveBeenCalledWith( expect(fs.writeFileSync).toHaveBeenCalledWith(
@@ -303,9 +297,7 @@ describe('Core System Prompt (prompts.ts)', () => {
}); });
it('should write to default path when GEMINI_WRITE_SYSTEM_MD is "1"', () => { it('should write to default path when GEMINI_WRITE_SYSTEM_MD is "1"', () => {
const defaultPath = path.resolve( const defaultPath = path.resolve(path.join(GEMINI_DIR, 'system.md'));
path.join(GEMINI_CONFIG_DIR, 'system.md'),
);
vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', '1'); vi.stubEnv('GEMINI_WRITE_SYSTEM_MD', '1');
getCoreSystemPrompt(mockConfig); getCoreSystemPrompt(mockConfig);
expect(fs.writeFileSync).toHaveBeenCalledWith( expect(fs.writeFileSync).toHaveBeenCalledWith(
+3 -2
View File
@@ -17,9 +17,10 @@ import { ShellTool } from '../tools/shell.js';
import { WRITE_FILE_TOOL_NAME } from '../tools/tool-names.js'; import { WRITE_FILE_TOOL_NAME } from '../tools/tool-names.js';
import process from 'node:process'; import process from 'node:process';
import { isGitRepository } from '../utils/gitUtils.js'; import { isGitRepository } from '../utils/gitUtils.js';
import { MemoryTool, GEMINI_CONFIG_DIR } from '../tools/memoryTool.js'; import { MemoryTool } from '../tools/memoryTool.js';
import { CodebaseInvestigatorAgent } from '../agents/codebase-investigator.js'; import { CodebaseInvestigatorAgent } from '../agents/codebase-investigator.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { GEMINI_DIR } from '../utils/paths.js';
export function resolvePathFromEnv(envVar?: string): { export function resolvePathFromEnv(envVar?: string): {
isSwitch: boolean; isSwitch: boolean;
@@ -78,7 +79,7 @@ export function getCoreSystemPrompt(
// A flag to indicate whether the system prompt override is active. // A flag to indicate whether the system prompt override is active.
let systemMdEnabled = false; let systemMdEnabled = false;
// The default path for the system prompt file. This can be overridden. // The default path for the system prompt file. This can be overridden.
let systemMdPath = path.resolve(path.join(GEMINI_CONFIG_DIR, 'system.md')); let systemMdPath = path.resolve(path.join(GEMINI_DIR, 'system.md'));
// Resolve the environment variable to get either a path or a switch value. // Resolve the environment variable to get either a path or a switch value.
const systemMdResolution = resolvePathFromEnv( const systemMdResolution = resolvePathFromEnv(
process.env['GEMINI_SYSTEM_MD'], process.env['GEMINI_SYSTEM_MD'],
@@ -9,6 +9,7 @@ import { promises as fs } from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { FileTokenStorage } from './file-token-storage.js'; import { FileTokenStorage } from './file-token-storage.js';
import type { OAuthCredentials } from './types.js'; import type { OAuthCredentials } from './types.js';
import { GEMINI_DIR } from '../../utils/paths.js';
vi.mock('node:fs', () => ({ vi.mock('node:fs', () => ({
promises: { promises: {
@@ -135,7 +136,7 @@ describe('FileTokenStorage', () => {
await storage.setCredentials(credentials); await storage.setCredentials(credentials);
expect(mockFs.mkdir).toHaveBeenCalledWith( expect(mockFs.mkdir).toHaveBeenCalledWith(
path.join('/home/test', '.gemini'), path.join('/home/test', GEMINI_DIR),
{ recursive: true, mode: 0o700 }, { recursive: true, mode: 0o700 },
); );
expect(mockFs.writeFile).toHaveBeenCalled(); expect(mockFs.writeFile).toHaveBeenCalled();
@@ -201,7 +202,7 @@ describe('FileTokenStorage', () => {
await storage.deleteCredentials('test-server'); await storage.deleteCredentials('test-server');
expect(mockFs.unlink).toHaveBeenCalledWith( expect(mockFs.unlink).toHaveBeenCalledWith(
path.join('/home/test', '.gemini', 'mcp-oauth-tokens-v2.json'), path.join('/home/test', GEMINI_DIR, 'mcp-oauth-tokens-v2.json'),
); );
}); });
@@ -282,7 +283,7 @@ describe('FileTokenStorage', () => {
await storage.clearAll(); await storage.clearAll();
expect(mockFs.unlink).toHaveBeenCalledWith( expect(mockFs.unlink).toHaveBeenCalledWith(
path.join('/home/test', '.gemini', 'mcp-oauth-tokens-v2.json'), path.join('/home/test', GEMINI_DIR, 'mcp-oauth-tokens-v2.json'),
); );
}); });
@@ -10,6 +10,7 @@ import * as os from 'node:os';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import { BaseTokenStorage } from './base-token-storage.js'; import { BaseTokenStorage } from './base-token-storage.js';
import type { OAuthCredentials } from './types.js'; import type { OAuthCredentials } from './types.js';
import { GEMINI_DIR } from '../../utils/paths.js';
export class FileTokenStorage extends BaseTokenStorage { export class FileTokenStorage extends BaseTokenStorage {
private readonly tokenFilePath: string; private readonly tokenFilePath: string;
@@ -17,7 +18,7 @@ export class FileTokenStorage extends BaseTokenStorage {
constructor(serviceName: string) { constructor(serviceName: string) {
super(serviceName); super(serviceName);
const configDir = path.join(os.homedir(), '.gemini'); const configDir = path.join(os.homedir(), GEMINI_DIR);
this.tokenFilePath = path.join(configDir, 'mcp-oauth-tokens-v2.json'); this.tokenFilePath = path.join(configDir, 'mcp-oauth-tokens-v2.json');
this.encryptionKey = this.deriveEncryptionKey(); this.encryptionKey = this.deriveEncryptionKey();
} }
+11 -8
View File
@@ -18,6 +18,7 @@ import * as path from 'node:path';
import * as os from 'node:os'; import * as os from 'node:os';
import { ToolConfirmationOutcome } from './tools.js'; import { ToolConfirmationOutcome } from './tools.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
import { GEMINI_DIR } from '../utils/paths.js';
// Mock dependencies // Mock dependencies
vi.mock(import('node:fs/promises'), async (importOriginal) => { vi.mock(import('node:fs/promises'), async (importOriginal) => {
@@ -105,7 +106,7 @@ describe('MemoryTool', () => {
beforeEach(() => { beforeEach(() => {
testFilePath = path.join( testFilePath = path.join(
os.homedir(), os.homedir(),
'.gemini', GEMINI_DIR,
DEFAULT_CONTEXT_FILENAME, DEFAULT_CONTEXT_FILENAME,
); );
}); });
@@ -237,7 +238,7 @@ describe('MemoryTool', () => {
// Use getCurrentGeminiMdFilename for the default expectation before any setGeminiMdFilename calls in a test // Use getCurrentGeminiMdFilename for the default expectation before any setGeminiMdFilename calls in a test
const expectedFilePath = path.join( const expectedFilePath = path.join(
os.homedir(), os.homedir(),
'.gemini', GEMINI_DIR,
getCurrentGeminiMdFilename(), // This will be DEFAULT_CONTEXT_FILENAME unless changed by a test getCurrentGeminiMdFilename(), // This will be DEFAULT_CONTEXT_FILENAME unless changed by a test
); );
@@ -317,9 +318,11 @@ describe('MemoryTool', () => {
expect(result).not.toBe(false); expect(result).not.toBe(false);
if (result && result.type === 'edit') { if (result && result.type === 'edit') {
const expectedPath = path.join('~', '.gemini', 'GEMINI.md'); const expectedPath = path.join('~', GEMINI_DIR, 'GEMINI.md');
expect(result.title).toBe(`Confirm Memory Save: ${expectedPath}`); expect(result.title).toBe(`Confirm Memory Save: ${expectedPath}`);
expect(result.fileName).toContain(path.join('mock', 'home', '.gemini')); expect(result.fileName).toContain(
path.join('mock', 'home', GEMINI_DIR),
);
expect(result.fileName).toContain('GEMINI.md'); expect(result.fileName).toContain('GEMINI.md');
expect(result.fileDiff).toContain('Index: GEMINI.md'); expect(result.fileDiff).toContain('Index: GEMINI.md');
expect(result.fileDiff).toContain('+## Gemini Added Memories'); expect(result.fileDiff).toContain('+## Gemini Added Memories');
@@ -334,7 +337,7 @@ describe('MemoryTool', () => {
const params = { fact: 'Test fact' }; const params = { fact: 'Test fact' };
const memoryFilePath = path.join( const memoryFilePath = path.join(
os.homedir(), os.homedir(),
'.gemini', GEMINI_DIR,
getCurrentGeminiMdFilename(), getCurrentGeminiMdFilename(),
); );
@@ -352,7 +355,7 @@ describe('MemoryTool', () => {
const params = { fact: 'Test fact' }; const params = { fact: 'Test fact' };
const memoryFilePath = path.join( const memoryFilePath = path.join(
os.homedir(), os.homedir(),
'.gemini', GEMINI_DIR,
getCurrentGeminiMdFilename(), getCurrentGeminiMdFilename(),
); );
@@ -378,7 +381,7 @@ describe('MemoryTool', () => {
const params = { fact: 'Test fact' }; const params = { fact: 'Test fact' };
const memoryFilePath = path.join( const memoryFilePath = path.join(
os.homedir(), os.homedir(),
'.gemini', GEMINI_DIR,
getCurrentGeminiMdFilename(), getCurrentGeminiMdFilename(),
); );
@@ -415,7 +418,7 @@ describe('MemoryTool', () => {
expect(result).not.toBe(false); expect(result).not.toBe(false);
if (result && result.type === 'edit') { if (result && result.type === 'edit') {
const expectedPath = path.join('~', '.gemini', 'GEMINI.md'); const expectedPath = path.join('~', GEMINI_DIR, 'GEMINI.md');
expect(result.title).toBe(`Confirm Memory Save: ${expectedPath}`); expect(result.title).toBe(`Confirm Memory Save: ${expectedPath}`);
expect(result.fileDiff).toContain('Index: GEMINI.md'); expect(result.fileDiff).toContain('Index: GEMINI.md');
expect(result.fileDiff).toContain('+- New fact'); expect(result.fileDiff).toContain('+- New fact');
+1 -2
View File
@@ -60,7 +60,6 @@ Do NOT use this tool:
- \`fact\` (string, required): The specific fact or piece of information to remember. This should be a clear, self-contained statement. For example, if the user says "My favorite color is blue", the fact would be "My favorite color is blue". - \`fact\` (string, required): The specific fact or piece of information to remember. This should be a clear, self-contained statement. For example, if the user says "My favorite color is blue", the fact would be "My favorite color is blue".
`; `;
export const GEMINI_CONFIG_DIR = '.gemini';
export const DEFAULT_CONTEXT_FILENAME = 'GEMINI.md'; export const DEFAULT_CONTEXT_FILENAME = 'GEMINI.md';
export const MEMORY_SECTION_HEADER = '## Gemini Added Memories'; export const MEMORY_SECTION_HEADER = '## Gemini Added Memories';
@@ -98,7 +97,7 @@ interface SaveMemoryParams {
modified_content?: string; modified_content?: string;
} }
function getGlobalMemoryFilePath(): string { export function getGlobalMemoryFilePath(): string {
return path.join(Storage.getGlobalGeminiDir(), getCurrentGeminiMdFilename()); return path.join(Storage.getGlobalGeminiDir(), getCurrentGeminiMdFilename());
} }
@@ -11,6 +11,7 @@ import * as os from 'node:os';
import { getFolderStructure } from './getFolderStructure.js'; import { getFolderStructure } from './getFolderStructure.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import * as path from 'node:path'; import * as path from 'node:path';
import { GEMINI_DIR } from './paths.js';
describe('getFolderStructure', () => { describe('getFolderStructure', () => {
let testRootDir: string; let testRootDir: string;
@@ -255,8 +256,8 @@ ${testRootDir}${path.sep}
await createTestFile('file1.txt'); await createTestFile('file1.txt');
await createTestFile('node_modules', 'some-package', 'index.js'); await createTestFile('node_modules', 'some-package', 'index.js');
await createTestFile('ignored.txt'); await createTestFile('ignored.txt');
await createTestFile('.gemini', 'config.yaml'); await createTestFile(GEMINI_DIR, 'config.yaml');
await createTestFile('.gemini', 'logs.json'); await createTestFile(GEMINI_DIR, 'logs.json');
const fileService = new FileDiscoveryService(testRootDir); const fileService = new FileDiscoveryService(testRootDir);
const structure = await getFolderStructure(testRootDir, { const structure = await getFolderStructure(testRootDir, {
@@ -301,8 +302,8 @@ ${testRootDir}${path.sep}
await createTestFile('file1.txt'); await createTestFile('file1.txt');
await createTestFile('node_modules', 'some-package', 'index.js'); await createTestFile('node_modules', 'some-package', 'index.js');
await createTestFile('ignored.txt'); await createTestFile('ignored.txt');
await createTestFile('.gemini', 'config.yaml'); await createTestFile(GEMINI_DIR, 'config.yaml');
await createTestFile('.gemini', 'logs.json'); await createTestFile(GEMINI_DIR, 'logs.json');
const fileService = new FileDiscoveryService(testRootDir); const fileService = new FileDiscoveryService(testRootDir);
const structure = await getFolderStructure(testRootDir, { const structure = await getFolderStructure(testRootDir, {
@@ -321,8 +322,8 @@ ${testRootDir}${path.sep}
await createTestFile('file1.txt'); await createTestFile('file1.txt');
await createTestFile('node_modules', 'some-package', 'index.js'); await createTestFile('node_modules', 'some-package', 'index.js');
await createTestFile('ignored.txt'); await createTestFile('ignored.txt');
await createTestFile('.gemini', 'config.yaml'); await createTestFile(GEMINI_DIR, 'config.yaml');
await createTestFile('.gemini', 'logs.json'); await createTestFile(GEMINI_DIR, 'logs.json');
const fileService = new FileDiscoveryService(testRootDir); const fileService = new FileDiscoveryService(testRootDir);
const structure = await getFolderStructure(testRootDir, { const structure = await getFolderStructure(testRootDir, {
@@ -11,6 +11,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os'; import * as os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { GEMINI_DIR } from './paths.js';
vi.mock('node:fs', async (importOriginal) => { vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>(); const actual = await importOriginal<typeof import('node:fs')>();
@@ -41,7 +42,7 @@ describe('InstallationManager', () => {
let tempHomeDir: string; let tempHomeDir: string;
let installationManager: InstallationManager; let installationManager: InstallationManager;
const installationIdFile = () => const installationIdFile = () =>
path.join(tempHomeDir, '.gemini', 'installation_id'); path.join(tempHomeDir, GEMINI_DIR, 'installation_id');
beforeEach(() => { beforeEach(() => {
tempHomeDir = fs.mkdtempSync( tempHomeDir = fs.mkdtempSync(
@@ -10,6 +10,7 @@ import { UserAccountManager } from './userAccountManager.js';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as os from 'node:os'; import * as os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { GEMINI_DIR } from './paths.js';
vi.mock('os', async (importOriginal) => { vi.mock('os', async (importOriginal) => {
const os = await importOriginal<typeof import('os')>(); const os = await importOriginal<typeof import('os')>();
@@ -30,7 +31,7 @@ describe('UserAccountManager', () => {
); );
(os.homedir as Mock).mockReturnValue(tempHomeDir); (os.homedir as Mock).mockReturnValue(tempHomeDir);
accountsFile = () => accountsFile = () =>
path.join(tempHomeDir, '.gemini', 'google_accounts.json'); path.join(tempHomeDir, GEMINI_DIR, 'google_accounts.json');
userAccountManager = new UserAccountManager(); userAccountManager = new UserAccountManager();
}); });
+3 -2
View File
@@ -25,6 +25,7 @@ import os from 'node:os';
import yargs from 'yargs'; import yargs from 'yargs';
import { hideBin } from 'yargs/helpers'; import { hideBin } from 'yargs/helpers';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { GEMINI_DIR } from '@google/gemini-cli-core';
const argv = yargs(hideBin(process.argv)).option('q', { const argv = yargs(hideBin(process.argv)).option('q', {
alias: 'quiet', alias: 'quiet',
@@ -35,7 +36,7 @@ const argv = yargs(hideBin(process.argv)).option('q', {
let geminiSandbox = process.env.GEMINI_SANDBOX; let geminiSandbox = process.env.GEMINI_SANDBOX;
if (!geminiSandbox) { if (!geminiSandbox) {
const userSettingsFile = join(os.homedir(), '.gemini', 'settings.json'); const userSettingsFile = join(os.homedir(), GEMINI_DIR, 'settings.json');
if (existsSync(userSettingsFile)) { if (existsSync(userSettingsFile)) {
const settings = JSON.parse( const settings = JSON.parse(
stripJsonComments(readFileSync(userSettingsFile, 'utf-8')), stripJsonComments(readFileSync(userSettingsFile, 'utf-8')),
@@ -49,7 +50,7 @@ if (!geminiSandbox) {
if (!geminiSandbox) { if (!geminiSandbox) {
let currentDir = process.cwd(); let currentDir = process.cwd();
while (true) { while (true) {
const geminiEnv = join(currentDir, '.gemini', '.env'); const geminiEnv = join(currentDir, GEMINI_DIR, '.env');
const regularEnv = join(currentDir, '.env'); const regularEnv = join(currentDir, '.env');
if (existsSync(geminiEnv)) { if (existsSync(geminiEnv)) {
dotenv.config({ path: geminiEnv, quiet: true }); dotenv.config({ path: geminiEnv, quiet: true });
+3 -7
View File
@@ -9,20 +9,16 @@
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import { join } from 'node:path'; import { join } from 'node:path';
import { existsSync, readFileSync } from 'node:fs'; import { existsSync, readFileSync } from 'node:fs';
import { GEMINI_DIR } from '@google/gemini-cli-core';
const projectRoot = join(import.meta.dirname, '..'); const projectRoot = join(import.meta.dirname, '..');
const SETTINGS_DIRECTORY_NAME = '.gemini';
const USER_SETTINGS_DIR = join( const USER_SETTINGS_DIR = join(
process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || '', process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || '',
SETTINGS_DIRECTORY_NAME, GEMINI_DIR,
); );
const USER_SETTINGS_PATH = join(USER_SETTINGS_DIR, 'settings.json'); const USER_SETTINGS_PATH = join(USER_SETTINGS_DIR, 'settings.json');
const WORKSPACE_SETTINGS_PATH = join( const WORKSPACE_SETTINGS_PATH = join(projectRoot, GEMINI_DIR, 'settings.json');
projectRoot,
SETTINGS_DIRECTORY_NAME,
'settings.json',
);
let settingsTarget = undefined; let settingsTarget = undefined;
+3 -2
View File
@@ -13,6 +13,7 @@ import os from 'node:os';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { GEMINI_DIR } from '@google/gemini-cli-core';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -24,9 +25,9 @@ const projectHash = crypto
.digest('hex'); .digest('hex');
// User-level .gemini directory in home // User-level .gemini directory in home
const USER_GEMINI_DIR = path.join(os.homedir(), '.gemini'); const USER_GEMINI_DIR = path.join(os.homedir(), GEMINI_DIR);
// Project-level .gemini directory in the workspace // Project-level .gemini directory in the workspace
const WORKSPACE_GEMINI_DIR = path.join(projectRoot, '.gemini'); const WORKSPACE_GEMINI_DIR = path.join(projectRoot, GEMINI_DIR);
// Telemetry artifacts are stored in a hashed directory under the user's ~/.gemini/tmp // Telemetry artifacts are stored in a hashed directory under the user's ~/.gemini/tmp
export const OTEL_DIR = path.join(USER_GEMINI_DIR, 'tmp', projectHash, 'otel'); export const OTEL_DIR = path.join(USER_GEMINI_DIR, 'tmp', projectHash, 'otel');