From 21c6480b65528a98ac0e1e3855f3c78c1f9b7cbe Mon Sep 17 00:00:00 2001 From: Yuki Okita Date: Wed, 20 Aug 2025 10:55:47 +0900 Subject: Refac: Centralize storage file management (#4078) Co-authored-by: Taylor Mullen --- packages/core/src/config/config.ts | 20 ++---- packages/core/src/config/storage.test.ts | 55 +++++++++++++++ packages/core/src/config/storage.ts | 114 +++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 packages/core/src/config/storage.test.ts create mode 100644 packages/core/src/config/storage.ts (limited to 'packages/core/src/config') diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 6a8e6d4b..39e885e2 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -22,16 +22,11 @@ import { ShellTool } from '../tools/shell.js'; import { WriteFileTool } from '../tools/write-file.js'; import { WebFetchTool } from '../tools/web-fetch.js'; import { ReadManyFilesTool } from '../tools/read-many-files.js'; -import { - MemoryTool, - setGeminiMdFilename, - GEMINI_CONFIG_DIR as GEMINI_DIR, -} from '../tools/memoryTool.js'; +import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js'; import { WebSearchTool } from '../tools/web-search.js'; import { GeminiClient } from '../core/client.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { GitService } from '../services/gitService.js'; -import { getProjectTempDir } from '../utils/paths.js'; import { initializeTelemetry, DEFAULT_TELEMETRY_TARGET, @@ -57,6 +52,7 @@ import { IdeConnectionEvent, IdeConnectionType } from '../telemetry/types.js'; // Re-export OAuth config type export type { MCPOAuthConfig }; import { WorkspaceContext } from '../utils/workspaceContext.js'; +import { Storage } from './storage.js'; export enum ApprovalMode { DEFAULT = 'default', @@ -272,6 +268,7 @@ export class Config { private readonly shouldUseNodePtyShell: boolean; private readonly skipNextSpeakerCheck: boolean; private initialized: boolean = false; + readonly storage: Storage; constructor(params: ConfigParameters) { this.sessionId = params.sessionId; @@ -340,6 +337,7 @@ export class Config { this.trustedFolder = params.trustedFolder; this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false; this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? false; + this.storage = new Storage(this.targetDir); if (params.contextFileName) { setGeminiMdFilename(params.contextFileName); @@ -591,14 +589,6 @@ export class Config { return this.geminiClient; } - getGeminiDir(): string { - return path.join(this.targetDir, GEMINI_DIR); - } - - getProjectTempDir(): string { - return getProjectTempDir(this.getProjectRoot()); - } - getEnableRecursiveFileSearch(): boolean { return this.fileFiltering.enableRecursiveFileSearch; } @@ -744,7 +734,7 @@ export class Config { async getGitService(): Promise { if (!this.gitService) { - this.gitService = new GitService(this.targetDir); + this.gitService = new GitService(this.targetDir, this.storage); await this.gitService.initialize(); } return this.gitService; diff --git a/packages/core/src/config/storage.test.ts b/packages/core/src/config/storage.test.ts new file mode 100644 index 00000000..4dab76f1 --- /dev/null +++ b/packages/core/src/config/storage.test.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi } from 'vitest'; +import * as os from 'os'; +import * as path from 'node:path'; + +vi.mock('fs', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + mkdirSync: vi.fn(), + }; +}); + +import { Storage } from './storage.js'; + +describe('Storage – getGlobalSettingsPath', () => { + it('returns path to ~/.gemini/settings.json', () => { + const expected = path.join(os.homedir(), '.gemini', 'settings.json'); + expect(Storage.getGlobalSettingsPath()).toBe(expected); + }); +}); + +describe('Storage – additional helpers', () => { + const projectRoot = '/tmp/project'; + const storage = new Storage(projectRoot); + + it('getWorkspaceSettingsPath returns project/.gemini/settings.json', () => { + const expected = path.join(projectRoot, '.gemini', 'settings.json'); + expect(storage.getWorkspaceSettingsPath()).toBe(expected); + }); + + it('getUserCommandsDir returns ~/.gemini/commands', () => { + const expected = path.join(os.homedir(), '.gemini', 'commands'); + expect(Storage.getUserCommandsDir()).toBe(expected); + }); + + it('getProjectCommandsDir returns project/.gemini/commands', () => { + const expected = path.join(projectRoot, '.gemini', 'commands'); + expect(storage.getProjectCommandsDir()).toBe(expected); + }); + + it('getMcpOAuthTokensPath returns ~/.gemini/mcp-oauth-tokens.json', () => { + const expected = path.join( + os.homedir(), + '.gemini', + 'mcp-oauth-tokens.json', + ); + expect(Storage.getMcpOAuthTokensPath()).toBe(expected); + }); +}); diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts new file mode 100644 index 00000000..1459c8c7 --- /dev/null +++ b/packages/core/src/config/storage.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'node:path'; +import * as os from 'os'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; + +export const GEMINI_DIR = '.gemini'; +export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json'; +const TMP_DIR_NAME = 'tmp'; + +export class Storage { + private readonly targetDir: string; + + constructor(targetDir: string) { + this.targetDir = targetDir; + } + + static getGlobalGeminiDir(): string { + const homeDir = os.homedir(); + if (!homeDir) { + return path.join(os.tmpdir(), '.gemini'); + } + return path.join(homeDir, GEMINI_DIR); + } + + static getMcpOAuthTokensPath(): string { + return path.join(Storage.getGlobalGeminiDir(), 'mcp-oauth-tokens.json'); + } + + static getGlobalSettingsPath(): string { + return path.join(Storage.getGlobalGeminiDir(), 'settings.json'); + } + + static getInstallationIdPath(): string { + return path.join(Storage.getGlobalGeminiDir(), 'installation_id'); + } + + static getGoogleAccountsPath(): string { + return path.join(Storage.getGlobalGeminiDir(), GOOGLE_ACCOUNTS_FILENAME); + } + + static getUserCommandsDir(): string { + return path.join(Storage.getGlobalGeminiDir(), 'commands'); + } + + static getGlobalMemoryFilePath(): string { + return path.join(Storage.getGlobalGeminiDir(), 'memory.md'); + } + + static getGlobalTempDir(): string { + return path.join(Storage.getGlobalGeminiDir(), TMP_DIR_NAME); + } + + getGeminiDir(): string { + return path.join(this.targetDir, GEMINI_DIR); + } + + getProjectTempDir(): string { + const hash = this.getFilePathHash(this.getProjectRoot()); + const tempDir = Storage.getGlobalTempDir(); + return path.join(tempDir, hash); + } + + ensureProjectTempDirExists(): void { + fs.mkdirSync(this.getProjectTempDir(), { recursive: true }); + } + + static getOAuthCredsPath(): string { + return path.join(Storage.getGlobalGeminiDir(), 'oauth_creds.json'); + } + + getProjectRoot(): string { + return this.targetDir; + } + + private getFilePathHash(filePath: string): string { + return crypto.createHash('sha256').update(filePath).digest('hex'); + } + + getHistoryDir(): string { + const hash = this.getFilePathHash(this.getProjectRoot()); + const historyDir = path.join(Storage.getGlobalGeminiDir(), 'history'); + return path.join(historyDir, hash); + } + + getWorkspaceSettingsPath(): string { + return path.join(this.getGeminiDir(), 'settings.json'); + } + + getProjectCommandsDir(): string { + return path.join(this.getGeminiDir(), 'commands'); + } + + getProjectTempCheckpointsDir(): string { + return path.join(this.getProjectTempDir(), 'checkpoints'); + } + + getExtensionsDir(): string { + return path.join(this.getGeminiDir(), 'extensions'); + } + + getExtensionsConfigPath(): string { + return path.join(this.getExtensionsDir(), 'gemini-extension.json'); + } + + getHistoryFilePath(): string { + return path.join(this.getProjectTempDir(), 'shell_history'); + } +} -- cgit v1.2.3