diff options
Diffstat (limited to 'packages/core/src/utils')
| -rw-r--r-- | packages/core/src/utils/editCorrector.test.ts | 1 | ||||
| -rw-r--r-- | packages/core/src/utils/installationManager.test.ts | 102 | ||||
| -rw-r--r-- | packages/core/src/utils/installationManager.ts | 58 | ||||
| -rw-r--r-- | packages/core/src/utils/paths.ts | 29 | ||||
| -rw-r--r-- | packages/core/src/utils/userAccountManager.test.ts (renamed from packages/core/src/utils/user_account.test.ts) | 70 | ||||
| -rw-r--r-- | packages/core/src/utils/userAccountManager.ts | 140 | ||||
| -rw-r--r-- | packages/core/src/utils/user_account.ts | 131 | ||||
| -rw-r--r-- | packages/core/src/utils/user_id.test.ts | 24 | ||||
| -rw-r--r-- | packages/core/src/utils/user_id.ts | 58 |
9 files changed, 336 insertions, 277 deletions
diff --git a/packages/core/src/utils/editCorrector.test.ts b/packages/core/src/utils/editCorrector.test.ts index cd588312..a7ef9522 100644 --- a/packages/core/src/utils/editCorrector.test.ts +++ b/packages/core/src/utils/editCorrector.test.ts @@ -27,6 +27,7 @@ let mockSendMessageStream: any; vi.mock('fs', () => ({ statSync: vi.fn(), + mkdirSync: vi.fn(), })); vi.mock('../core/client.js', () => ({ diff --git a/packages/core/src/utils/installationManager.test.ts b/packages/core/src/utils/installationManager.test.ts new file mode 100644 index 00000000..d6a35f68 --- /dev/null +++ b/packages/core/src/utils/installationManager.test.ts @@ -0,0 +1,102 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { vi, describe, it, expect, beforeEach, afterEach, Mock } from 'vitest'; +import { InstallationManager } from './installationManager.js'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import path from 'node:path'; +import { randomUUID } from 'crypto'; + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal<typeof import('node:fs')>(); + return { + ...actual, + readFileSync: vi.fn(actual.readFileSync), + existsSync: vi.fn(actual.existsSync), + } as typeof actual; +}); + +vi.mock('os', async (importOriginal) => { + const os = await importOriginal<typeof import('os')>(); + return { + ...os, + homedir: vi.fn(), + }; +}); + +vi.mock('crypto', async (importOriginal) => { + const crypto = await importOriginal<typeof import('crypto')>(); + return { + ...crypto, + randomUUID: vi.fn(), + }; +}); + +describe('InstallationManager', () => { + let tempHomeDir: string; + let installationManager: InstallationManager; + const installationIdFile = () => + path.join(tempHomeDir, '.gemini', 'installation_id'); + + beforeEach(() => { + tempHomeDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'gemini-cli-test-home-'), + ); + (os.homedir as Mock).mockReturnValue(tempHomeDir); + installationManager = new InstallationManager(); + }); + + afterEach(() => { + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + vi.clearAllMocks(); + }); + + describe('getInstallationId', () => { + it('should create and write a new installation ID if one does not exist', () => { + const newId = 'new-uuid-123'; + (randomUUID as Mock).mockReturnValue(newId); + + const installationId = installationManager.getInstallationId(); + + expect(installationId).toBe(newId); + expect(fs.existsSync(installationIdFile())).toBe(true); + expect(fs.readFileSync(installationIdFile(), 'utf-8')).toBe(newId); + }); + + it('should read an existing installation ID from a file', () => { + const existingId = 'existing-uuid-123'; + fs.mkdirSync(path.dirname(installationIdFile()), { recursive: true }); + fs.writeFileSync(installationIdFile(), existingId); + + const installationId = installationManager.getInstallationId(); + + expect(installationId).toBe(existingId); + }); + + it('should return the same ID on subsequent calls', () => { + const firstId = installationManager.getInstallationId(); + const secondId = installationManager.getInstallationId(); + expect(secondId).toBe(firstId); + }); + + it('should handle read errors and return a fallback ID', () => { + vi.mocked(fs.existsSync).mockReturnValueOnce(true); + const readSpy = vi.mocked(fs.readFileSync); + readSpy.mockImplementationOnce(() => { + throw new Error('Read error'); + }); + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const id = installationManager.getInstallationId(); + + expect(id).toBe('123456789'); + expect(consoleErrorSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/core/src/utils/installationManager.ts b/packages/core/src/utils/installationManager.ts new file mode 100644 index 00000000..9146ddd0 --- /dev/null +++ b/packages/core/src/utils/installationManager.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'fs'; +import { randomUUID } from 'crypto'; +import * as path from 'node:path'; +import { Storage } from '../config/storage.js'; + +export class InstallationManager { + private getInstallationIdPath(): string { + return Storage.getInstallationIdPath(); + } + + private readInstallationIdFromFile(): string | null { + const installationIdFile = this.getInstallationIdPath(); + if (fs.existsSync(installationIdFile)) { + const installationid = fs + .readFileSync(installationIdFile, 'utf-8') + .trim(); + return installationid || null; + } + return null; + } + + private writeInstallationIdToFile(installationId: string) { + const installationIdFile = this.getInstallationIdPath(); + const dir = path.dirname(installationIdFile); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(installationIdFile, installationId, 'utf-8'); + } + + /** + * Retrieves the installation ID from a file, creating it if it doesn't exist. + * This ID is used for unique user installation tracking. + * @returns A UUID string for the user. + */ + getInstallationId(): string { + try { + let installationId = this.readInstallationIdFromFile(); + + if (!installationId) { + installationId = randomUUID(); + this.writeInstallationIdToFile(installationId); + } + + return installationId; + } catch (error) { + console.error( + 'Error accessing installation ID file, generating ephemeral ID:', + error, + ); + return '123456789'; + } + } +} diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index e7cf54cc..fe690b39 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -10,8 +10,6 @@ import * as crypto from 'crypto'; export const GEMINI_DIR = '.gemini'; export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json'; -const TMP_DIR_NAME = 'tmp'; -const COMMANDS_DIR_NAME = 'commands'; /** * Special characters that need to be escaped in file paths for shell compatibility. @@ -175,33 +173,6 @@ export function getProjectHash(projectRoot: string): string { } /** - * 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); -} - -/** - * Returns the absolute path to the user-level commands directory. - * @returns The path to the user's commands directory. - */ -export function getUserCommandsDir(): string { - return path.join(os.homedir(), GEMINI_DIR, COMMANDS_DIR_NAME); -} - -/** - * Returns the absolute path to the project-level commands directory. - * @param projectRoot The absolute path to the project's root directory. - * @returns The path to the project's commands directory. - */ -export function getProjectCommandsDir(projectRoot: string): string { - return path.join(projectRoot, GEMINI_DIR, COMMANDS_DIR_NAME); -} - -/** * Checks if a path is a subpath of another path. * @param parentPath The parent path. * @param childPath The child path. diff --git a/packages/core/src/utils/user_account.test.ts b/packages/core/src/utils/userAccountManager.test.ts index 35231ca3..1e7e05aa 100644 --- a/packages/core/src/utils/user_account.test.ts +++ b/packages/core/src/utils/userAccountManager.test.ts @@ -5,12 +5,7 @@ */ import { vi, describe, it, expect, beforeEach, afterEach, Mock } from 'vitest'; -import { - cacheGoogleAccount, - getCachedGoogleAccount, - clearCachedGoogleAccount, - getLifetimeGoogleAccounts, -} from './user_account.js'; +import { UserAccountManager } from './userAccountManager.js'; import * as fs from 'node:fs'; import * as os from 'node:os'; import path from 'node:path'; @@ -23,16 +18,21 @@ vi.mock('os', async (importOriginal) => { }; }); -describe('user_account', () => { +describe('UserAccountManager', () => { let tempHomeDir: string; - const accountsFile = () => - path.join(tempHomeDir, '.gemini', 'google_accounts.json'); + let userAccountManager: UserAccountManager; + let accountsFile: () => string; + beforeEach(() => { tempHomeDir = fs.mkdtempSync( path.join(os.tmpdir(), 'gemini-cli-test-home-'), ); (os.homedir as Mock).mockReturnValue(tempHomeDir); + accountsFile = () => + path.join(tempHomeDir, '.gemini', 'google_accounts.json'); + userAccountManager = new UserAccountManager(); }); + afterEach(() => { fs.rmSync(tempHomeDir, { recursive: true, force: true }); vi.clearAllMocks(); @@ -40,7 +40,7 @@ describe('user_account', () => { describe('cacheGoogleAccount', () => { it('should create directory and write initial account file', async () => { - await cacheGoogleAccount('[email protected]'); + await userAccountManager.cacheGoogleAccount('[email protected]'); // Verify Google Account ID was cached expect(fs.existsSync(accountsFile())).toBe(true); @@ -60,7 +60,7 @@ describe('user_account', () => { ), ); - await cacheGoogleAccount('[email protected]'); + await userAccountManager.cacheGoogleAccount('[email protected]'); expect(fs.readFileSync(accountsFile(), 'utf-8')).toBe( JSON.stringify( @@ -84,8 +84,8 @@ describe('user_account', () => { 2, ), ); - await cacheGoogleAccount('[email protected]'); - await cacheGoogleAccount('[email protected]'); + await userAccountManager.cacheGoogleAccount('[email protected]'); + await userAccountManager.cacheGoogleAccount('[email protected]'); expect(fs.readFileSync(accountsFile(), 'utf-8')).toBe( JSON.stringify( @@ -103,7 +103,7 @@ describe('user_account', () => { .spyOn(console, 'log') .mockImplementation(() => {}); - await cacheGoogleAccount('[email protected]'); + await userAccountManager.cacheGoogleAccount('[email protected]'); expect(consoleLogSpy).toHaveBeenCalled(); expect(JSON.parse(fs.readFileSync(accountsFile(), 'utf-8'))).toEqual({ @@ -122,7 +122,7 @@ describe('user_account', () => { .spyOn(console, 'log') .mockImplementation(() => {}); - await cacheGoogleAccount('[email protected]'); + await userAccountManager.cacheGoogleAccount('[email protected]'); expect(consoleLogSpy).toHaveBeenCalled(); expect(JSON.parse(fs.readFileSync(accountsFile(), 'utf-8'))).toEqual({ @@ -139,19 +139,19 @@ describe('user_account', () => { accountsFile(), JSON.stringify({ active: '[email protected]', old: [] }, null, 2), ); - const account = getCachedGoogleAccount(); + const account = userAccountManager.getCachedGoogleAccount(); expect(account).toBe('[email protected]'); }); it('should return null if file does not exist', () => { - const account = getCachedGoogleAccount(); + const account = userAccountManager.getCachedGoogleAccount(); expect(account).toBeNull(); }); it('should return null if file is empty', () => { fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); fs.writeFileSync(accountsFile(), ''); - const account = getCachedGoogleAccount(); + const account = userAccountManager.getCachedGoogleAccount(); expect(account).toBeNull(); }); @@ -162,7 +162,7 @@ describe('user_account', () => { .spyOn(console, 'log') .mockImplementation(() => {}); - const account = getCachedGoogleAccount(); + const account = userAccountManager.getCachedGoogleAccount(); expect(account).toBeNull(); expect(consoleLogSpy).toHaveBeenCalled(); @@ -171,7 +171,7 @@ describe('user_account', () => { it('should return null if active key is missing', () => { fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); fs.writeFileSync(accountsFile(), JSON.stringify({ old: [] })); - const account = getCachedGoogleAccount(); + const account = userAccountManager.getCachedGoogleAccount(); expect(account).toBeNull(); }); }); @@ -188,7 +188,7 @@ describe('user_account', () => { ), ); - await clearCachedGoogleAccount(); + await userAccountManager.clearCachedGoogleAccount(); const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); expect(stored.active).toBeNull(); @@ -198,7 +198,7 @@ describe('user_account', () => { it('should handle empty file gracefully', async () => { fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); fs.writeFileSync(accountsFile(), ''); - await clearCachedGoogleAccount(); + await userAccountManager.clearCachedGoogleAccount(); const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); expect(stored.active).toBeNull(); expect(stored.old).toEqual([]); @@ -211,7 +211,7 @@ describe('user_account', () => { .spyOn(console, 'log') .mockImplementation(() => {}); - await clearCachedGoogleAccount(); + await userAccountManager.clearCachedGoogleAccount(); expect(consoleLogSpy).toHaveBeenCalled(); const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); @@ -226,7 +226,7 @@ describe('user_account', () => { JSON.stringify({ active: null, old: ['[email protected]'] }, null, 2), ); - await clearCachedGoogleAccount(); + await userAccountManager.clearCachedGoogleAccount(); const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); expect(stored.active).toBeNull(); @@ -247,7 +247,7 @@ describe('user_account', () => { ), ); - await clearCachedGoogleAccount(); + await userAccountManager.clearCachedGoogleAccount(); const stored = JSON.parse(fs.readFileSync(accountsFile(), 'utf-8')); expect(stored.active).toBeNull(); @@ -257,24 +257,24 @@ describe('user_account', () => { describe('getLifetimeGoogleAccounts', () => { it('should return 0 if the file does not exist', () => { - expect(getLifetimeGoogleAccounts()).toBe(0); + expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0); }); it('should return 0 if the file is empty', () => { fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); fs.writeFileSync(accountsFile(), ''); - expect(getLifetimeGoogleAccounts()).toBe(0); + expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0); }); it('should return 0 if the file is corrupted', () => { fs.mkdirSync(path.dirname(accountsFile()), { recursive: true }); fs.writeFileSync(accountsFile(), 'invalid json'); - const consoleLogSpy = vi + const consoleDebugSpy = vi .spyOn(console, 'log') .mockImplementation(() => {}); - expect(getLifetimeGoogleAccounts()).toBe(0); - expect(consoleLogSpy).toHaveBeenCalled(); + expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0); + expect(consoleDebugSpy).toHaveBeenCalled(); }); it('should return 1 if there is only an active account', () => { @@ -283,7 +283,7 @@ describe('user_account', () => { accountsFile(), JSON.stringify({ active: '[email protected]', old: [] }), ); - expect(getLifetimeGoogleAccounts()).toBe(1); + expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(1); }); it('should correctly count old accounts when active is null', () => { @@ -295,7 +295,7 @@ describe('user_account', () => { old: ['[email protected]', '[email protected]'], }), ); - expect(getLifetimeGoogleAccounts()).toBe(2); + expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(2); }); it('should correctly count both active and old accounts', () => { @@ -307,7 +307,7 @@ describe('user_account', () => { old: ['[email protected]', '[email protected]'], }), ); - expect(getLifetimeGoogleAccounts()).toBe(3); + expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(3); }); it('should handle valid JSON with incorrect schema by returning 0', () => { @@ -320,7 +320,7 @@ describe('user_account', () => { .spyOn(console, 'log') .mockImplementation(() => {}); - expect(getLifetimeGoogleAccounts()).toBe(0); + expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(0); expect(consoleLogSpy).toHaveBeenCalled(); }); @@ -333,7 +333,7 @@ describe('user_account', () => { old: ['[email protected]', '[email protected]'], }), ); - expect(getLifetimeGoogleAccounts()).toBe(2); + expect(userAccountManager.getLifetimeGoogleAccounts()).toBe(2); }); }); }); diff --git a/packages/core/src/utils/userAccountManager.ts b/packages/core/src/utils/userAccountManager.ts new file mode 100644 index 00000000..28d3cef9 --- /dev/null +++ b/packages/core/src/utils/userAccountManager.ts @@ -0,0 +1,140 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import path from 'node:path'; +import { promises as fsp, readFileSync } from 'node:fs'; +import { Storage } from '../config/storage.js'; + +interface UserAccounts { + active: string | null; + old: string[]; +} + +export class UserAccountManager { + private getGoogleAccountsCachePath(): string { + return Storage.getGoogleAccountsPath(); + } + + /** + * Parses and validates the string content of an accounts file. + * @param content The raw string content from the file. + * @returns A valid UserAccounts object. + */ + private parseAndValidateAccounts(content: string): UserAccounts { + const defaultState = { active: null, old: [] }; + if (!content.trim()) { + return defaultState; + } + + const parsed = JSON.parse(content); + + // Inlined validation logic + if (typeof parsed !== 'object' || parsed === null) { + console.log('Invalid accounts file schema, starting fresh.'); + return defaultState; + } + const { active, old } = parsed as Partial<UserAccounts>; + const isValid = + (active === undefined || active === null || typeof active === 'string') && + (old === undefined || + (Array.isArray(old) && old.every((i) => typeof i === 'string'))); + + if (!isValid) { + console.log('Invalid accounts file schema, starting fresh.'); + return defaultState; + } + + return { + active: parsed.active ?? null, + old: parsed.old ?? [], + }; + } + + private readAccountsSync(filePath: string): UserAccounts { + const defaultState = { active: null, old: [] }; + try { + const content = readFileSync(filePath, 'utf-8'); + return this.parseAndValidateAccounts(content); + } catch (error) { + if ( + error instanceof Error && + 'code' in error && + error.code === 'ENOENT' + ) { + return defaultState; + } + console.log('Error during sync read of accounts, starting fresh.', error); + return defaultState; + } + } + + private async readAccounts(filePath: string): Promise<UserAccounts> { + const defaultState = { active: null, old: [] }; + try { + const content = await fsp.readFile(filePath, 'utf-8'); + return this.parseAndValidateAccounts(content); + } catch (error) { + if ( + error instanceof Error && + 'code' in error && + error.code === 'ENOENT' + ) { + return defaultState; + } + console.log('Could not parse accounts file, starting fresh.', error); + return defaultState; + } + } + + async cacheGoogleAccount(email: string): Promise<void> { + const filePath = this.getGoogleAccountsCachePath(); + await fsp.mkdir(path.dirname(filePath), { recursive: true }); + + const accounts = await this.readAccounts(filePath); + + if (accounts.active && accounts.active !== email) { + if (!accounts.old.includes(accounts.active)) { + accounts.old.push(accounts.active); + } + } + + // If the new email was in the old list, remove it + accounts.old = accounts.old.filter((oldEmail) => oldEmail !== email); + + accounts.active = email; + await fsp.writeFile(filePath, JSON.stringify(accounts, null, 2), 'utf-8'); + } + + getCachedGoogleAccount(): string | null { + const filePath = this.getGoogleAccountsCachePath(); + const accounts = this.readAccountsSync(filePath); + return accounts.active; + } + + getLifetimeGoogleAccounts(): number { + const filePath = this.getGoogleAccountsCachePath(); + const accounts = this.readAccountsSync(filePath); + const allAccounts = new Set(accounts.old); + if (accounts.active) { + allAccounts.add(accounts.active); + } + return allAccounts.size; + } + + async clearCachedGoogleAccount(): Promise<void> { + const filePath = this.getGoogleAccountsCachePath(); + const accounts = await this.readAccounts(filePath); + + if (accounts.active) { + if (!accounts.old.includes(accounts.active)) { + accounts.old.push(accounts.active); + } + accounts.active = null; + } + + await fsp.writeFile(filePath, JSON.stringify(accounts, null, 2), 'utf-8'); + } +} diff --git a/packages/core/src/utils/user_account.ts b/packages/core/src/utils/user_account.ts deleted file mode 100644 index 18b7dcf4..00000000 --- a/packages/core/src/utils/user_account.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import path from 'node:path'; -import { promises as fsp, readFileSync } from 'node:fs'; -import * as os from 'os'; -import { GEMINI_DIR, GOOGLE_ACCOUNTS_FILENAME } from './paths.js'; - -interface UserAccounts { - active: string | null; - old: string[]; -} - -function getGoogleAccountsCachePath(): string { - return path.join(os.homedir(), GEMINI_DIR, GOOGLE_ACCOUNTS_FILENAME); -} - -/** - * Parses and validates the string content of an accounts file. - * @param content The raw string content from the file. - * @returns A valid UserAccounts object. - */ -function parseAndValidateAccounts(content: string): UserAccounts { - const defaultState = { active: null, old: [] }; - if (!content.trim()) { - return defaultState; - } - - const parsed = JSON.parse(content); - - // Inlined validation logic - if (typeof parsed !== 'object' || parsed === null) { - console.log('Invalid accounts file schema, starting fresh.'); - return defaultState; - } - const { active, old } = parsed as Partial<UserAccounts>; - const isValid = - (active === undefined || active === null || typeof active === 'string') && - (old === undefined || - (Array.isArray(old) && old.every((i) => typeof i === 'string'))); - - if (!isValid) { - console.log('Invalid accounts file schema, starting fresh.'); - return defaultState; - } - - return { - active: parsed.active ?? null, - old: parsed.old ?? [], - }; -} - -function readAccountsSync(filePath: string): UserAccounts { - const defaultState = { active: null, old: [] }; - try { - const content = readFileSync(filePath, 'utf-8'); - return parseAndValidateAccounts(content); - } catch (error) { - if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { - return defaultState; - } - console.log('Error during sync read of accounts, starting fresh.', error); - return defaultState; - } -} - -async function readAccounts(filePath: string): Promise<UserAccounts> { - const defaultState = { active: null, old: [] }; - try { - const content = await fsp.readFile(filePath, 'utf-8'); - return parseAndValidateAccounts(content); - } catch (error) { - if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { - return defaultState; - } - console.log('Could not parse accounts file, starting fresh.', error); - return defaultState; - } -} - -export async function cacheGoogleAccount(email: string): Promise<void> { - const filePath = getGoogleAccountsCachePath(); - await fsp.mkdir(path.dirname(filePath), { recursive: true }); - - const accounts = await readAccounts(filePath); - - if (accounts.active && accounts.active !== email) { - if (!accounts.old.includes(accounts.active)) { - accounts.old.push(accounts.active); - } - } - - // If the new email was in the old list, remove it - accounts.old = accounts.old.filter((oldEmail) => oldEmail !== email); - - accounts.active = email; - await fsp.writeFile(filePath, JSON.stringify(accounts, null, 2), 'utf-8'); -} - -export function getCachedGoogleAccount(): string | null { - const filePath = getGoogleAccountsCachePath(); - const accounts = readAccountsSync(filePath); - return accounts.active; -} - -export function getLifetimeGoogleAccounts(): number { - const filePath = getGoogleAccountsCachePath(); - const accounts = readAccountsSync(filePath); - const allAccounts = new Set(accounts.old); - if (accounts.active) { - allAccounts.add(accounts.active); - } - return allAccounts.size; -} - -export async function clearCachedGoogleAccount(): Promise<void> { - const filePath = getGoogleAccountsCachePath(); - const accounts = await readAccounts(filePath); - - if (accounts.active) { - if (!accounts.old.includes(accounts.active)) { - accounts.old.push(accounts.active); - } - accounts.active = null; - } - - await fsp.writeFile(filePath, JSON.stringify(accounts, null, 2), 'utf-8'); -} diff --git a/packages/core/src/utils/user_id.test.ts b/packages/core/src/utils/user_id.test.ts deleted file mode 100644 index 5c11d773..00000000 --- a/packages/core/src/utils/user_id.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect } from 'vitest'; -import { getInstallationId } from './user_id.js'; - -describe('user_id', () => { - describe('getInstallationId', () => { - it('should return a valid UUID format string', () => { - const installationId = getInstallationId(); - - expect(installationId).toBeDefined(); - expect(typeof installationId).toBe('string'); - expect(installationId.length).toBeGreaterThan(0); - - // Should return the same ID on subsequent calls (consistent) - const secondCall = getInstallationId(); - expect(secondCall).toBe(installationId); - }); - }); -}); diff --git a/packages/core/src/utils/user_id.ts b/packages/core/src/utils/user_id.ts deleted file mode 100644 index 6f16806f..00000000 --- a/packages/core/src/utils/user_id.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as os from 'os'; -import * as fs from 'fs'; -import * as path from 'path'; -import { randomUUID } from 'crypto'; -import { GEMINI_DIR } from './paths.js'; - -const homeDir = os.homedir() ?? ''; -const geminiDir = path.join(homeDir, GEMINI_DIR); -const installationIdFile = path.join(geminiDir, 'installation_id'); - -function ensureGeminiDirExists() { - if (!fs.existsSync(geminiDir)) { - fs.mkdirSync(geminiDir, { recursive: true }); - } -} - -function readInstallationIdFromFile(): string | null { - if (fs.existsSync(installationIdFile)) { - const installationid = fs.readFileSync(installationIdFile, 'utf-8').trim(); - return installationid || null; - } - return null; -} - -function writeInstallationIdToFile(installationId: string) { - fs.writeFileSync(installationIdFile, installationId, 'utf-8'); -} - -/** - * Retrieves the installation ID from a file, creating it if it doesn't exist. - * This ID is used for unique user installation tracking. - * @returns A UUID string for the user. - */ -export function getInstallationId(): string { - try { - ensureGeminiDirExists(); - let installationId = readInstallationIdFromFile(); - - if (!installationId) { - installationId = randomUUID(); - writeInstallationIdToFile(installationId); - } - - return installationId; - } catch (error) { - console.error( - 'Error accessing installation ID file, generating ephemeral ID:', - error, - ); - return '123456789'; - } -} |
