summaryrefslogtreecommitdiff
path: root/packages/core/src/utils
diff options
context:
space:
mode:
authorYuki Okita <[email protected]>2025-08-20 10:55:47 +0900
committerGitHub <[email protected]>2025-08-20 01:55:47 +0000
commit21c6480b65528a98ac0e1e3855f3c78c1f9b7cbe (patch)
tree5555ec429209e87e0c21483c9e5fddd53ac01dbc /packages/core/src/utils
parent1049d388451120587a8643a401fd71430a8cd5fe (diff)
Refac: Centralize storage file management (#4078)
Co-authored-by: Taylor Mullen <[email protected]>
Diffstat (limited to 'packages/core/src/utils')
-rw-r--r--packages/core/src/utils/editCorrector.test.ts1
-rw-r--r--packages/core/src/utils/installationManager.test.ts102
-rw-r--r--packages/core/src/utils/installationManager.ts58
-rw-r--r--packages/core/src/utils/paths.ts29
-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.ts140
-rw-r--r--packages/core/src/utils/user_account.ts131
-rw-r--r--packages/core/src/utils/user_id.test.ts24
-rw-r--r--packages/core/src/utils/user_id.ts58
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', () => {
}),
);
- 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', () => {
}),
);
- 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', () => {
}),
);
- 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';
- }
-}