diff --git a/packages/cli/src/ui/hooks/useShellHistory.test.ts b/packages/cli/src/ui/hooks/useShellHistory.test.ts index 47fc5c62..8d030497 100644 --- a/packages/cli/src/ui/hooks/useShellHistory.test.ts +++ b/packages/cli/src/ui/hooks/useShellHistory.test.ts @@ -7,16 +7,30 @@ import { renderHook, act, waitFor } from '@testing-library/react'; import { useShellHistory } from './useShellHistory.js'; import * as fs from 'fs/promises'; -import path from 'path'; +import * as path from 'path'; +import * as os from 'os'; +import * as crypto from 'crypto'; vi.mock('fs/promises'); +vi.mock('os'); +vi.mock('crypto'); const MOCKED_PROJECT_ROOT = '/test/project'; -const MOCKED_HISTORY_DIR = path.join(MOCKED_PROJECT_ROOT, '.gemini'); +const MOCKED_HOME_DIR = '/test/home'; +const MOCKED_PROJECT_HASH = 'mocked_hash'; + +const MOCKED_HISTORY_DIR = path.join( + MOCKED_HOME_DIR, + '.gemini', + 'tmp', + MOCKED_PROJECT_HASH, +); const MOCKED_HISTORY_FILE = path.join(MOCKED_HISTORY_DIR, 'shell_history'); describe('useShellHistory', () => { const mockedFs = vi.mocked(fs); + const mockedOs = vi.mocked(os); + const mockedCrypto = vi.mocked(crypto); beforeEach(() => { vi.resetAllMocks(); @@ -24,6 +38,13 @@ describe('useShellHistory', () => { mockedFs.readFile.mockResolvedValue(''); mockedFs.writeFile.mockResolvedValue(undefined); mockedFs.mkdir.mockResolvedValue(undefined); + mockedOs.homedir.mockReturnValue(MOCKED_HOME_DIR); + + const hashMock = { + update: vi.fn().mockReturnThis(), + digest: vi.fn().mockReturnValue(MOCKED_PROJECT_HASH), + }; + mockedCrypto.createHash.mockReturnValue(hashMock as never); }); it('should initialize and read the history file from the correct path', async () => { diff --git a/packages/cli/src/ui/hooks/useShellHistory.ts b/packages/cli/src/ui/hooks/useShellHistory.ts index 0b1c8d98..507a18de 100644 --- a/packages/cli/src/ui/hooks/useShellHistory.ts +++ b/packages/cli/src/ui/hooks/useShellHistory.ts @@ -7,14 +7,13 @@ import { useState, useEffect, useCallback } from 'react'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { isNodeError } from '@gemini-cli/core'; +import { isNodeError, getProjectTempDir } from '@gemini-cli/core'; -const HISTORY_DIR = '.gemini'; const HISTORY_FILE = 'shell_history'; const MAX_HISTORY_LENGTH = 100; async function getHistoryFilePath(projectRoot: string): Promise { - const historyDir = path.join(projectRoot, HISTORY_DIR); + const historyDir = getProjectTempDir(projectRoot); return path.join(historyDir, HISTORY_FILE); } diff --git a/packages/core/src/core/logger.ts b/packages/core/src/core/logger.ts index ea512399..4aeac542 100644 --- a/packages/core/src/core/logger.ts +++ b/packages/core/src/core/logger.ts @@ -5,13 +5,10 @@ */ import path from 'node:path'; -import os from 'node:os'; -import crypto from 'node:crypto'; import { promises as fs } from 'node:fs'; import { Content } from '@google/genai'; +import { getProjectTempDir } from '../utils/paths.js'; -const GEMINI_DIR = '.gemini'; -const TMP_DIR_NAME = 'tmp'; const LOG_FILE_NAME = 'logs.json'; const CHECKPOINT_FILE_NAME = 'checkpoint.json'; @@ -99,17 +96,7 @@ export class Logger { return; } - const projectHash = crypto - .createHash('sha256') - .update(process.cwd()) - .digest('hex'); - - this.geminiDir = path.join( - os.homedir(), - GEMINI_DIR, - TMP_DIR_NAME, - projectHash, - ); + this.geminiDir = getProjectTempDir(process.cwd()); this.logFilePath = path.join(this.geminiDir, LOG_FILE_NAME); this.checkpointFilePath = path.join(this.geminiDir, CHECKPOINT_FILE_NAME); diff --git a/packages/core/src/services/gitService.ts b/packages/core/src/services/gitService.ts index 956dcec0..83f1fec2 100644 --- a/packages/core/src/services/gitService.ts +++ b/packages/core/src/services/gitService.ts @@ -7,11 +7,11 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; -import * as crypto from 'crypto'; import { isNodeError } from '../utils/errors.js'; import { isGitRepository } from '../utils/gitUtils.js'; import { exec } from 'node:child_process'; import { simpleGit, SimpleGit, CheckRepoActions } from 'simple-git'; +import { getProjectHash, GEMINI_DIR } from '../utils/paths.js'; export class GitService { private projectRoot: string; @@ -21,11 +21,8 @@ export class GitService { } private getHistoryDir(): string { - const hash = crypto - .createHash('sha256') - .update(this.projectRoot) - .digest('hex'); - return path.join(os.homedir(), '.gemini', 'history', hash); + const hash = getProjectHash(this.projectRoot); + return path.join(os.homedir(), GEMINI_DIR, 'history', hash); } async initialize(): Promise { diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index 28f2f1f0..28ca5cbc 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -6,6 +6,10 @@ import path from 'node:path'; import os from 'os'; +import * as crypto from 'crypto'; + +export const GEMINI_DIR = '.gemini'; +const TMP_DIR_NAME = 'tmp'; /** * Replaces the home directory with a tilde. @@ -134,3 +138,22 @@ export function escapePath(filePath: string): string { export function unescapePath(filePath: string): string { return filePath.replace(/\\ /g, ' '); } + +/** + * Generates a unique hash for a project based on its root path. + * @param projectRoot The absolute path to the project's root directory. + * @returns A SHA256 hash of the project root path. + */ +export function getProjectHash(projectRoot: string): string { + return crypto.createHash('sha256').update(projectRoot).digest('hex'); +} + +/** + * Generates a unique temporary directory path for a project. + * @param projectRoot The absolute path to the project's root directory. + * @returns The path to the project's temporary directory. + */ +export function getProjectTempDir(projectRoot: string): string { + const hash = getProjectHash(projectRoot); + return path.join(os.homedir(), GEMINI_DIR, TMP_DIR_NAME, hash); +}