Introduce GEMINI_CLI_HOME for strict test isolation (#15907)

This commit is contained in:
N. Taylor Mullen
2026-01-06 20:09:39 -08:00
committed by GitHub
parent a26463b056
commit 7956eb239e
54 changed files with 455 additions and 148 deletions
+32
View File
@@ -169,6 +169,38 @@ export default tseslint.config(
'@typescript-eslint/await-thenable': ['error'], '@typescript-eslint/await-thenable': ['error'],
'@typescript-eslint/no-floating-promises': ['error'], '@typescript-eslint/no-floating-promises': ['error'],
'@typescript-eslint/no-unnecessary-type-assertion': ['error'], '@typescript-eslint/no-unnecessary-type-assertion': ['error'],
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'node:os',
importNames: ['homedir', 'tmpdir'],
message:
'Please use the helpers from @google/gemini-cli-core instead of node:os homedir()/tmpdir() to ensure strict environment isolation.',
},
{
name: 'os',
importNames: ['homedir', 'tmpdir'],
message:
'Please use the helpers from @google/gemini-cli-core instead of os homedir()/tmpdir() to ensure strict environment isolation.',
},
],
},
],
},
},
{
// Allow os.homedir() in tests and paths.ts where it is used to implement the helper
files: [
'**/*.test.ts',
'**/*.test.tsx',
'packages/core/src/utils/paths.ts',
'packages/test-utils/src/**/*.ts',
'scripts/**/*.js',
],
rules: {
'no-restricted-imports': 'off',
}, },
}, },
{ {
+25 -8
View File
@@ -274,6 +274,7 @@ export class InteractiveRun {
export class TestRig { export class TestRig {
testDir: string | null = null; testDir: string | null = null;
homeDir: string | null = null;
testName?: string; testName?: string;
_lastRunStdout?: string; _lastRunStdout?: string;
// Path to the copied fake responses file for this test. // Path to the copied fake responses file for this test.
@@ -294,7 +295,9 @@ export class TestRig {
const testFileDir = const testFileDir =
env['INTEGRATION_TEST_FILE_DIR'] || join(os.tmpdir(), 'gemini-cli-tests'); env['INTEGRATION_TEST_FILE_DIR'] || join(os.tmpdir(), 'gemini-cli-tests');
this.testDir = join(testFileDir, sanitizedName); this.testDir = join(testFileDir, sanitizedName);
this.homeDir = join(testFileDir, sanitizedName + '-home');
mkdirSync(this.testDir, { recursive: true }); mkdirSync(this.testDir, { recursive: true });
mkdirSync(this.homeDir, { recursive: true });
if (options.fakeResponsesPath) { if (options.fakeResponsesPath) {
this.fakeResponsesPath = join(this.testDir, 'fake-responses.json'); this.fakeResponsesPath = join(this.testDir, 'fake-responses.json');
this.originalFakeResponsesPath = options.fakeResponsesPath; this.originalFakeResponsesPath = options.fakeResponsesPath;
@@ -304,11 +307,15 @@ export class TestRig {
} }
// 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_DIR); const projectGeminiDir = join(this.testDir, GEMINI_DIR);
mkdirSync(geminiDir, { recursive: true }); mkdirSync(projectGeminiDir, { 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
const telemetryPath = join(this.testDir, 'telemetry.log'); // Always use test directory for telemetry const telemetryPath = join(this.homeDir, 'telemetry.log'); // Always use home directory for telemetry
// Ensure the CLI uses our separate home directory for global state
process.env['GEMINI_CLI_HOME'] = this.homeDir;
const settings = { const settings = {
general: { general: {
@@ -339,7 +346,7 @@ export class TestRig {
...options.settings, // Allow tests to override/add settings ...options.settings, // Allow tests to override/add settings
}; };
writeFileSync( writeFileSync(
join(geminiDir, 'settings.json'), join(projectGeminiDir, 'settings.json'),
JSON.stringify(settings, null, 2), JSON.stringify(settings, null, 2),
); );
} }
@@ -595,7 +602,7 @@ export class TestRig {
) { ) {
fs.copyFileSync(this.fakeResponsesPath, this.originalFakeResponsesPath!); fs.copyFileSync(this.fakeResponsesPath, this.originalFakeResponsesPath!);
} }
// Clean up test directory // Clean up test directory and home directory
if (this.testDir && !env['KEEP_OUTPUT']) { if (this.testDir && !env['KEEP_OUTPUT']) {
try { try {
fs.rmSync(this.testDir, { recursive: true, force: true }); fs.rmSync(this.testDir, { recursive: true, force: true });
@@ -606,11 +613,21 @@ export class TestRig {
} }
} }
} }
if (this.homeDir && !env['KEEP_OUTPUT']) {
try {
fs.rmSync(this.homeDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
if (env['VERBOSE'] === 'true') {
console.warn('Cleanup warning:', (error as Error).message);
}
}
}
} }
async waitForTelemetryReady() { async waitForTelemetryReady() {
// Telemetry is always written to the test directory // Telemetry is always written to the test directory
const logFilePath = join(this.testDir!, 'telemetry.log'); const logFilePath = join(this.homeDir!, 'telemetry.log');
if (!logFilePath) return; if (!logFilePath) return;
@@ -861,7 +878,7 @@ export class TestRig {
private _readAndParseTelemetryLog(): ParsedLog[] { private _readAndParseTelemetryLog(): ParsedLog[] {
// Telemetry is always written to the test directory // Telemetry is always written to the test directory
const logFilePath = join(this.testDir!, 'telemetry.log'); const logFilePath = join(this.homeDir!, 'telemetry.log');
if (!logFilePath || !fs.existsSync(logFilePath)) { if (!logFilePath || !fs.existsSync(logFilePath)) {
return []; return [];
@@ -903,7 +920,7 @@ export class TestRig {
// If not, fall back to parsing from stdout // If not, fall back to parsing from stdout
if (env['GEMINI_SANDBOX'] === 'podman') { if (env['GEMINI_SANDBOX'] === 'podman') {
// Try reading from file first // Try reading from file first
const logFilePath = join(this.testDir!, 'telemetry.log'); const logFilePath = join(this.homeDir!, 'telemetry.log');
if (fs.existsSync(logFilePath)) { if (fs.existsSync(logFilePath)) {
try { try {
@@ -7,53 +7,79 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Command } from './types.js'; import type { Command } from './types.js';
describe('CommandRegistry', () => { const {
const mockListExtensionsCommandInstance: Command = { mockExtensionsCommand,
mockListExtensionsCommand,
mockExtensionsCommandInstance,
mockListExtensionsCommandInstance,
} = vi.hoisted(() => {
const listInstance: Command = {
name: 'extensions list', name: 'extensions list',
description: 'Lists all installed extensions.', description: 'Lists all installed extensions.',
execute: vi.fn(), execute: vi.fn(),
}; };
const mockListExtensionsCommand = vi.fn(
() => mockListExtensionsCommandInstance,
);
const mockExtensionsCommandInstance: Command = { const extInstance: Command = {
name: 'extensions', name: 'extensions',
description: 'Manage extensions.', description: 'Manage extensions.',
execute: vi.fn(), execute: vi.fn(),
subCommands: [mockListExtensionsCommandInstance], subCommands: [listInstance],
}; };
const mockExtensionsCommand = vi.fn(() => mockExtensionsCommandInstance);
return {
mockListExtensionsCommandInstance: listInstance,
mockExtensionsCommandInstance: extInstance,
mockExtensionsCommand: vi.fn(() => extInstance),
mockListExtensionsCommand: vi.fn(() => listInstance),
};
});
vi.mock('./extensions.js', () => ({
ExtensionsCommand: mockExtensionsCommand,
ListExtensionsCommand: mockListExtensionsCommand,
}));
vi.mock('./init.js', () => ({
InitCommand: vi.fn(() => ({
name: 'init',
description: 'Initializes the server.',
execute: vi.fn(),
})),
}));
vi.mock('./restore.js', () => ({
RestoreCommand: vi.fn(() => ({
name: 'restore',
description: 'Restores the server.',
execute: vi.fn(),
})),
}));
import { commandRegistry } from './command-registry.js';
describe('CommandRegistry', () => {
beforeEach(async () => { beforeEach(async () => {
vi.resetModules(); vi.clearAllMocks();
vi.doMock('./extensions.js', () => ({ commandRegistry.initialize();
ExtensionsCommand: mockExtensionsCommand,
ListExtensionsCommand: mockListExtensionsCommand,
}));
}); });
it('should register ExtensionsCommand on initialization', async () => { it('should register ExtensionsCommand on initialization', async () => {
const { commandRegistry } = await import('./command-registry.js');
expect(mockExtensionsCommand).toHaveBeenCalled(); expect(mockExtensionsCommand).toHaveBeenCalled();
const command = commandRegistry.get('extensions'); const command = commandRegistry.get('extensions');
expect(command).toBe(mockExtensionsCommandInstance); expect(command).toBe(mockExtensionsCommandInstance);
}); });
it('should register sub commands on initialization', async () => { it('should register sub commands on initialization', async () => {
const { commandRegistry } = await import('./command-registry.js');
const command = commandRegistry.get('extensions list'); const command = commandRegistry.get('extensions list');
expect(command).toBe(mockListExtensionsCommandInstance); expect(command).toBe(mockListExtensionsCommandInstance);
}); });
it('get() should return undefined for a non-existent command', async () => { it('get() should return undefined for a non-existent command', async () => {
const { commandRegistry } = await import('./command-registry.js');
const command = commandRegistry.get('non-existent'); const command = commandRegistry.get('non-existent');
expect(command).toBeUndefined(); expect(command).toBeUndefined();
}); });
it('register() should register a new command', async () => { it('register() should register a new command', async () => {
const { commandRegistry } = await import('./command-registry.js');
const mockCommand: Command = { const mockCommand: Command = {
name: 'test-command', name: 'test-command',
description: '', description: '',
@@ -65,7 +91,6 @@ describe('CommandRegistry', () => {
}); });
it('register() should register a nested command', async () => { it('register() should register a nested command', async () => {
const { commandRegistry } = await import('./command-registry.js');
const mockSubSubCommand: Command = { const mockSubSubCommand: Command = {
name: 'test-command-sub-sub', name: 'test-command-sub-sub',
description: '', description: '',
@@ -95,8 +120,8 @@ describe('CommandRegistry', () => {
}); });
it('register() should not enter an infinite loop with a cyclic command', async () => { it('register() should not enter an infinite loop with a cyclic command', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const { debugLogger } = await import('@google/gemini-cli-core');
const { commandRegistry } = await import('./command-registry.js'); const warnSpy = vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
const mockCommand: Command = { const mockCommand: Command = {
name: 'cyclic-command', name: 'cyclic-command',
description: '', description: '',
@@ -112,7 +137,6 @@ describe('CommandRegistry', () => {
expect(warnSpy).toHaveBeenCalledWith( expect(warnSpy).toHaveBeenCalledWith(
'Command cyclic-command already registered. Skipping.', 'Command cyclic-command already registered. Skipping.',
); );
// If the test finishes, it means we didn't get into an infinite loop.
warnSpy.mockRestore(); warnSpy.mockRestore();
}); });
}); });
@@ -10,10 +10,15 @@ import { InitCommand } from './init.js';
import { RestoreCommand } from './restore.js'; import { RestoreCommand } from './restore.js';
import type { Command } from './types.js'; import type { Command } from './types.js';
class CommandRegistry { export class CommandRegistry {
private readonly commands = new Map<string, Command>(); private readonly commands = new Map<string, Command>();
constructor() { constructor() {
this.initialize();
}
initialize() {
this.commands.clear();
this.register(new ExtensionsCommand()); this.register(new ExtensionsCommand());
this.register(new RestoreCommand()); this.register(new RestoreCommand());
this.register(new InitCommand()); this.register(new InitCommand());
+1 -1
View File
@@ -6,7 +6,6 @@
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 { homedir } from 'node:os';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import type { TelemetryTarget } from '@google/gemini-cli-core'; import type { TelemetryTarget } from '@google/gemini-cli-core';
@@ -23,6 +22,7 @@ import {
type ExtensionLoader, type ExtensionLoader,
startupProfiler, startupProfiler,
PREVIEW_GEMINI_MODEL, PREVIEW_GEMINI_MODEL,
homedir,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
+2 -2
View File
@@ -11,10 +11,10 @@ import {
type MCPServerConfig, type MCPServerConfig,
type ExtensionInstallMetadata, type ExtensionInstallMetadata,
type GeminiCLIExtension, type GeminiCLIExtension,
homedir,
} 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 { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions'); export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
@@ -39,7 +39,7 @@ interface ExtensionConfig {
export function loadExtensions(workspaceDir: string): GeminiCLIExtension[] { export function loadExtensions(workspaceDir: string): GeminiCLIExtension[] {
const allExtensions = [ const allExtensions = [
...loadExtensionsFromDir(workspaceDir), ...loadExtensionsFromDir(workspaceDir),
...loadExtensionsFromDir(os.homedir()), ...loadExtensionsFromDir(homedir()),
]; ];
const uniqueExtensions: GeminiCLIExtension[] = []; const uniqueExtensions: GeminiCLIExtension[] = [];
@@ -27,13 +27,21 @@ vi.mock('node:os', async (importOriginal) => {
}; };
}); });
vi.mock('@google/gemini-cli-core', () => ({ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
GEMINI_DIR: '.gemini', const actual =
debugLogger: { await importOriginal<typeof import('@google/gemini-cli-core')>();
error: vi.fn(), const path = await import('node:path');
}, const os = await import('node:os');
getErrorMessage: (error: unknown) => String(error), return {
})); ...actual,
GEMINI_DIR: '.gemini',
debugLogger: {
error: vi.fn(),
},
getErrorMessage: (error: unknown) => String(error),
homedir: () => path.join(os.tmpdir(), `gemini-home-${mocks.suffix}`),
};
});
describe('loadSettings', () => { describe('loadSettings', () => {
const mockHomeDir = path.join(os.tmpdir(), `gemini-home-${mocks.suffix}`); const mockHomeDir = path.join(os.tmpdir(), `gemini-home-${mocks.suffix}`);
+1 -1
View File
@@ -6,7 +6,6 @@
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 { homedir } from 'node:os';
import type { MCPServerConfig } from '@google/gemini-cli-core'; import type { MCPServerConfig } from '@google/gemini-cli-core';
import { import {
@@ -14,6 +13,7 @@ import {
GEMINI_DIR, GEMINI_DIR,
getErrorMessage, getErrorMessage,
type TelemetrySettings, type TelemetrySettings,
homedir,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import stripJsonComments from 'strip-json-comments'; import stripJsonComments from 'strip-json-comments';
+1 -1
View File
@@ -9,7 +9,7 @@ import { gzipSync, gunzipSync } from 'node:zlib';
import * as tar from 'tar'; import * as tar from 'tar';
import * as fse from 'fs-extra'; import * as fse from 'fs-extra';
import { promises as fsPromises, createReadStream } from 'node:fs'; import { promises as fsPromises, createReadStream } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from '@google/gemini-cli-core';
import { join } from 'node:path'; import { join } from 'node:path';
import type { Task as SDKTask } from '@a2a-js/sdk'; import type { Task as SDKTask } from '@a2a-js/sdk';
import type { TaskStore } from '@a2a-js/sdk/server'; import type { TaskStore } from '@a2a-js/sdk/server';
@@ -24,6 +24,15 @@ vi.mock('os', async (importOriginal) => {
}; };
}); });
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: mockHomedir,
};
});
describe('ExtensionManager skills validation', () => { describe('ExtensionManager skills validation', () => {
let tempHomeDir: string; let tempHomeDir: string;
let tempWorkspaceDir: string; let tempWorkspaceDir: string;
+4 -4
View File
@@ -6,7 +6,6 @@
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 { stat } from 'node:fs/promises'; import { stat } from 'node:fs/promises';
import chalk from 'chalk'; import chalk from 'chalk';
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
@@ -39,6 +38,7 @@ import {
logExtensionUninstall, logExtensionUninstall,
logExtensionUpdateEvent, logExtensionUpdateEvent,
loadSkillsFromDir, loadSkillsFromDir,
homedir,
type ExtensionEvents, type ExtensionEvents,
type MCPServerConfig, type MCPServerConfig,
type ExtensionInstallMetadata, type ExtensionInstallMetadata,
@@ -692,7 +692,7 @@ Would you like to attempt to install via "git clone" instead?`,
toOutputString(extension: GeminiCLIExtension): string { toOutputString(extension: GeminiCLIExtension): string {
const userEnabled = this.extensionEnablementManager.isEnabled( const userEnabled = this.extensionEnablementManager.isEnabled(
extension.name, extension.name,
os.homedir(), homedir(),
); );
const workspaceEnabled = this.extensionEnablementManager.isEnabled( const workspaceEnabled = this.extensionEnablementManager.isEnabled(
extension.name, extension.name,
@@ -766,7 +766,7 @@ Would you like to attempt to install via "git clone" instead?`,
if (scope !== SettingScope.Session) { if (scope !== SettingScope.Session) {
const scopePath = const scopePath =
scope === SettingScope.Workspace ? this.workspaceDir : os.homedir(); scope === SettingScope.Workspace ? this.workspaceDir : homedir();
this.extensionEnablementManager.disable(name, true, scopePath); this.extensionEnablementManager.disable(name, true, scopePath);
} }
await logExtensionDisable( await logExtensionDisable(
@@ -801,7 +801,7 @@ Would you like to attempt to install via "git clone" instead?`,
if (scope !== SettingScope.Session) { if (scope !== SettingScope.Session) {
const scopePath = const scopePath =
scope === SettingScope.Workspace ? this.workspaceDir : os.homedir(); scope === SettingScope.Workspace ? this.workspaceDir : homedir();
this.extensionEnablementManager.enable(name, true, scopePath); this.extensionEnablementManager.enable(name, true, scopePath);
} }
await logExtensionEnable( await logExtensionEnable(
@@ -105,6 +105,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
logExtensionUninstall: mockLogExtensionUninstall, logExtensionUninstall: mockLogExtensionUninstall,
logExtensionUpdateEvent: mockLogExtensionUpdateEvent, logExtensionUpdateEvent: mockLogExtensionUpdateEvent,
logExtensionDisable: mockLogExtensionDisable, logExtensionDisable: mockLogExtensionDisable,
homedir: mockHomedir,
ExtensionEnableEvent: vi.fn(), ExtensionEnableEvent: vi.fn(),
ExtensionInstallEvent: vi.fn(), ExtensionInstallEvent: vi.fn(),
ExtensionUninstallEvent: vi.fn(), ExtensionUninstallEvent: vi.fn(),
@@ -11,7 +11,7 @@ import {
EXTENSION_SETTINGS_FILENAME, EXTENSION_SETTINGS_FILENAME,
EXTENSIONS_CONFIG_FILENAME, EXTENSIONS_CONFIG_FILENAME,
} from './variables.js'; } from './variables.js';
import { Storage } from '@google/gemini-cli-core'; import { Storage, homedir } from '@google/gemini-cli-core';
export class ExtensionStorage { export class ExtensionStorage {
private readonly extensionName: string; private readonly extensionName: string;
@@ -36,7 +36,7 @@ export class ExtensionStorage {
} }
static getUserExtensionsDir(): string { static getUserExtensionsDir(): string {
return new Storage(os.homedir()).getExtensionsDir(); return new Storage(homedir()).getExtensionsDir();
} }
static async createTmpDir(): Promise<string> { static async createTmpDir(): Promise<string> {
+2 -1
View File
@@ -6,7 +6,7 @@
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 { homedir, platform } from 'node:os'; import { platform } from 'node:os';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import process from 'node:process'; import process from 'node:process';
import { import {
@@ -16,6 +16,7 @@ import {
getErrorMessage, getErrorMessage,
Storage, Storage,
coreEvents, coreEvents,
homedir,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import stripJsonComments from 'strip-json-comments'; import stripJsonComments from 'strip-json-comments';
import { DefaultLight } from '../ui/themes/default-light.js'; import { DefaultLight } from '../ui/themes/default-light.js';
@@ -27,6 +27,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
return { return {
...actual, ...actual,
coreEvents: mockCoreEvents, coreEvents: mockCoreEvents,
homedir: () => '/mock/home/user',
Storage: class extends actual.Storage { Storage: class extends actual.Storage {
static override getGlobalSettingsPath = () => static override getGlobalSettingsPath = () =>
'/mock/home/user/.gemini/settings.json'; '/mock/home/user/.gemini/settings.json';
@@ -52,11 +53,15 @@ vi.mock('./trustedFolders.js', () => ({
}, },
})); }));
vi.mock('os', () => ({ vi.mock('os', async (importOriginal) => {
homedir: () => '/mock/home/user', const actual = await importOriginal<typeof import('node:os')>();
platform: () => 'linux', return {
totalmem: () => 16 * 1024 * 1024 * 1024, ...actual,
})); homedir: () => '/mock/home/user',
platform: () => 'linux',
totalmem: () => 16 * 1024 * 1024 * 1024,
};
});
vi.mock('fs', async (importOriginal) => { vi.mock('fs', async (importOriginal) => {
const actualFs = await importOriginal<typeof fs>(); const actualFs = await importOriginal<typeof fs>();
@@ -36,6 +36,15 @@ vi.mock('os', async (importOriginal) => {
platform: vi.fn(() => 'linux'), platform: vi.fn(() => 'linux'),
}; };
}); });
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: () => '/mock/home/user',
};
});
vi.mock('fs', async (importOriginal) => { vi.mock('fs', async (importOriginal) => {
const actualFs = await importOriginal<typeof fs>(); const actualFs = await importOriginal<typeof fs>();
return { return {
+1 -1
View File
@@ -6,13 +6,13 @@
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 { homedir } from 'node:os';
import { import {
FatalConfigError, FatalConfigError,
getErrorMessage, getErrorMessage,
isWithinRoot, isWithinRoot,
ideContextStore, ideContextStore,
GEMINI_DIR, GEMINI_DIR,
homedir,
} 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';
@@ -48,6 +48,7 @@ vi.mock('node:path', async () => {
vi.mock('@google/gemini-cli-core', () => ({ vi.mock('@google/gemini-cli-core', () => ({
GEMINI_DIR: '.gemini', GEMINI_DIR: '.gemini',
homedir: () => '/mock/home',
Storage: { Storage: {
getGlobalTempDir: () => '/mock/temp', getGlobalTempDir: () => '/mock/temp',
}, },
@@ -12,13 +12,17 @@ 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, Storage, debugLogger } from '@google/gemini-cli-core'; import {
GEMINI_DIR,
Storage,
debugLogger,
homedir,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
const settingsPath = path.join(os.homedir(), GEMINI_DIR, 'settings.json'); const settingsPath = path.join(homedir(), GEMINI_DIR, 'settings.json');
const screenReaderNudgeFilePath = path.join( const screenReaderNudgeFilePath = path.join(
Storage.getGlobalTempDir(), Storage.getGlobalTempDir(),
@@ -74,6 +74,7 @@ vi.mock('node:process', () => {
exit: mockProcessExit, exit: mockProcessExit,
platform: 'sunos', platform: 'sunos',
cwd: () => '/fake/dir', cwd: () => '/fake/dir',
env: {},
} as unknown as NodeJS.Process; } as unknown as NodeJS.Process;
return { return {
...mockProcess, ...mockProcess,
@@ -30,6 +30,15 @@ vi.mock('os', async (importOriginal) => {
}; };
}); });
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: () => os.homedir(),
};
});
vi.mock('../../config/extensions/update.js', () => ({ vi.mock('../../config/extensions/update.js', () => ({
checkForAllExtensionUpdates: vi.fn(), checkForAllExtensionUpdates: vi.fn(),
updateExtension: vi.fn(), updateExtension: vi.fn(),
@@ -28,9 +28,16 @@ const mockedIsWorkspaceTrusted = vi.hoisted(() => vi.fn());
const mockedUseSettings = vi.hoisted(() => vi.fn()); const mockedUseSettings = vi.hoisted(() => vi.fn());
// Mock modules // Mock modules
vi.mock('node:process', () => ({ vi.mock('node:process', () => {
cwd: mockedCwd, const mockProcess = {
})); cwd: mockedCwd,
env: {},
};
return {
...mockProcess,
default: mockProcess,
};
});
vi.mock('node:path', async (importOriginal) => { vi.mock('node:path', async (importOriginal) => {
const actual = await importOriginal(); const actual = await importOriginal();
@@ -27,6 +27,15 @@ vi.mock('node:os', async (importOriginal) => {
}; };
}); });
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: () => os.homedir(),
};
});
const validCustomTheme: CustomTheme = { const validCustomTheme: CustomTheme = {
type: 'custom', type: 'custom',
name: 'MyCustomTheme', name: 'MyCustomTheme',
+2 -3
View File
@@ -18,7 +18,6 @@ import { ShadesOfPurple } from './shades-of-purple.js';
import { XCode } from './xcode.js'; import { XCode } from './xcode.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 type { Theme, ThemeType, CustomTheme } from './theme.js'; import type { Theme, ThemeType, CustomTheme } from './theme.js';
import { createCustomTheme, validateCustomTheme } from './theme.js'; import { createCustomTheme, validateCustomTheme } from './theme.js';
import type { SemanticColors } from './semantic-tokens.js'; import type { SemanticColors } from './semantic-tokens.js';
@@ -26,7 +25,7 @@ import { ANSI } from './ansi.js';
import { ANSILight } from './ansi-light.js'; import { ANSILight } from './ansi-light.js';
import { NoColorTheme } from './no-color.js'; import { NoColorTheme } from './no-color.js';
import process from 'node:process'; import process from 'node:process';
import { debugLogger } from '@google/gemini-cli-core'; import { debugLogger, homedir } from '@google/gemini-cli-core';
export interface ThemeDisplay { export interface ThemeDisplay {
name: string; name: string;
@@ -255,7 +254,7 @@ class ThemeManager {
} }
// 2. Perform security check. // 2. Perform security check.
const homeDir = path.resolve(os.homedir()); const homeDir = path.resolve(homedir());
if (!canonicalPath.startsWith(homeDir)) { if (!canonicalPath.startsWith(homeDir)) {
debugLogger.warn( debugLogger.warn(
`Theme file at "${themePath}" is outside your home directory. ` + `Theme file at "${themePath}" is outside your home directory. ` +
@@ -16,6 +16,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
await importOriginal<typeof import('@google/gemini-cli-core')>(); await importOriginal<typeof import('@google/gemini-cli-core')>();
return { return {
...original, ...original,
homedir: () => mockHomeDir,
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({ loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
memoryContent: 'mock memory', memoryContent: 'mock memory',
fileCount: 10, fileCount: 10,
+4 -4
View File
@@ -4,10 +4,10 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import * as os from 'node:os';
import * as path from 'node:path'; import * as path from 'node:path';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { opendir } from 'node:fs/promises'; import { opendir } from 'node:fs/promises';
import { homedir } from '@google/gemini-cli-core';
const MAX_SUGGESTIONS = 50; const MAX_SUGGESTIONS = 50;
const MATCH_BUFFER_MULTIPLIER = 3; const MATCH_BUFFER_MULTIPLIER = 3;
@@ -18,9 +18,9 @@ export function expandHomeDir(p: string): string {
} }
let expandedPath = p; let expandedPath = p;
if (p.toLowerCase().startsWith('%userprofile%')) { if (p.toLowerCase().startsWith('%userprofile%')) {
expandedPath = os.homedir() + p.substring('%userprofile%'.length); expandedPath = homedir() + p.substring('%userprofile%'.length);
} else if (p === '~' || p.startsWith('~/')) { } else if (p === '~' || p.startsWith('~/')) {
expandedPath = os.homedir() + p.substring(1); expandedPath = homedir() + p.substring(1);
} }
return path.normalize(expandedPath); return path.normalize(expandedPath);
} }
@@ -56,7 +56,7 @@ function parsePartialPath(partialPath: string): ParsedPath {
!partialPath.includes('/') && !partialPath.includes('/') &&
!partialPath.includes(path.sep) !partialPath.includes(path.sep)
) { ) {
searchDir = os.homedir(); searchDir = homedir();
filter = partialPath.substring(1); filter = partialPath.substring(1);
} }
} }
@@ -42,6 +42,15 @@ vi.mock('node:os', () => ({
platform: mocks.platform, platform: mocks.platform,
})); }));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: mocks.homedir,
};
});
vi.mock('./terminalCapabilityManager.js', () => ({ vi.mock('./terminalCapabilityManager.js', () => ({
terminalCapabilityManager: { terminalCapabilityManager: {
isKittyProtocolEnabled: vi.fn().mockReturnValue(false), isKittyProtocolEnabled: vi.fn().mockReturnValue(false),
+3 -3
View File
@@ -30,7 +30,7 @@ import { exec } from 'node:child_process';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { terminalCapabilityManager } from './terminalCapabilityManager.js'; import { terminalCapabilityManager } from './terminalCapabilityManager.js';
import { debugLogger } from '@google/gemini-cli-core'; import { debugLogger, homedir } from '@google/gemini-cli-core';
export const VSCODE_SHIFT_ENTER_SEQUENCE = '\\\r\n'; export const VSCODE_SHIFT_ENTER_SEQUENCE = '\\\r\n';
@@ -124,7 +124,7 @@ function getVSCodeStyleConfigDir(appName: string): string | null {
if (platform === 'darwin') { if (platform === 'darwin') {
return path.join( return path.join(
os.homedir(), homedir(),
'Library', 'Library',
'Application Support', 'Application Support',
appName, appName,
@@ -136,7 +136,7 @@ function getVSCodeStyleConfigDir(appName: string): string | null {
} }
return path.join(process.env['APPDATA'], appName, 'User'); return path.join(process.env['APPDATA'], appName, 'User');
} else { } else {
return path.join(os.homedir(), '.config', appName, 'User'); return path.join(homedir(), '.config', appName, 'User');
} }
} }
@@ -13,6 +13,10 @@ vi.mock('node:os', () => ({
homedir: vi.fn(), homedir: vi.fn(),
})); }));
vi.mock('@google/gemini-cli-core', () => ({
homedir: () => os.homedir(),
}));
describe('resolvePath', () => { describe('resolvePath', () => {
beforeEach(() => { beforeEach(() => {
vi.mocked(os.homedir).mockReturnValue('/home/user'); vi.mocked(os.homedir).mockReturnValue('/home/user');
+3 -3
View File
@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import * as os from 'node:os';
import * as path from 'node:path'; import * as path from 'node:path';
import { homedir } from '@google/gemini-cli-core';
export function resolvePath(p: string): string { export function resolvePath(p: string): string {
if (!p) { if (!p) {
@@ -13,9 +13,9 @@ export function resolvePath(p: string): string {
} }
let expandedPath = p; let expandedPath = p;
if (p.toLowerCase().startsWith('%userprofile%')) { if (p.toLowerCase().startsWith('%userprofile%')) {
expandedPath = os.homedir() + p.substring('%userprofile%'.length); expandedPath = homedir() + p.substring('%userprofile%'.length);
} else if (p === '~' || p.startsWith('~/')) { } else if (p === '~' || p.startsWith('~/')) {
expandedPath = os.homedir() + p.substring(1); expandedPath = homedir() + p.substring(1);
} }
return path.normalize(expandedPath); return path.normalize(expandedPath);
} }
+26 -5
View File
@@ -12,9 +12,19 @@ import { start_sandbox } from './sandbox.js';
import { FatalSandboxError, type SandboxConfig } from '@google/gemini-cli-core'; import { FatalSandboxError, type SandboxConfig } from '@google/gemini-cli-core';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
vi.mock('../config/settings.js', () => ({ const { mockedHomedir, mockedGetContainerPath } = vi.hoisted(() => ({
USER_SETTINGS_DIR: '/home/user/.gemini', mockedHomedir: vi.fn().mockReturnValue('/home/user'),
mockedGetContainerPath: vi.fn().mockImplementation((p: string) => p),
})); }));
vi.mock('./sandboxUtils.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./sandboxUtils.js')>();
return {
...actual,
getContainerPath: mockedGetContainerPath,
};
});
vi.mock('node:child_process'); vi.mock('node:child_process');
vi.mock('node:os'); vi.mock('node:os');
vi.mock('node:fs'); vi.mock('node:fs');
@@ -44,6 +54,7 @@ vi.mock('node:util', async (importOriginal) => {
}, },
}; };
}); });
vi.mock('@google/gemini-cli-core', async (importOriginal) => { vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual = const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>(); await importOriginal<typeof import('@google/gemini-cli-core')>();
@@ -64,7 +75,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
} }
}, },
GEMINI_DIR: '.gemini', GEMINI_DIR: '.gemini',
USER_SETTINGS_DIR: '/home/user/.gemini', homedir: mockedHomedir,
}; };
}); });
@@ -341,13 +352,23 @@ describe('sandbox', () => {
await start_sandbox(config); await start_sandbox(config);
expect(spawn).toHaveBeenCalledWith( // The first call is 'docker images -q ...'
expect(spawn).toHaveBeenNthCalledWith(
1,
'docker',
expect.arrayContaining(['images', '-q']),
);
// The second call is 'docker run ...'
expect(spawn).toHaveBeenNthCalledWith(
2,
'docker', 'docker',
expect.arrayContaining([ expect.arrayContaining([
'run',
'--volume', '--volume',
'/host/path:/container/path:ro', '/host/path:/container/path:ro',
'--volume', '--volume',
expect.stringContaining('/home/user/.gemini'), expect.stringMatching(/[\\/]home[\\/]user[\\/]\.gemini/),
]), ]),
expect.any(Object), expect.any(Object),
); );
+23 -10
View File
@@ -5,12 +5,11 @@
*/ */
import { exec, execSync, spawn, type ChildProcess } from 'node:child_process'; import { exec, execSync, spawn, type ChildProcess } from 'node:child_process';
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { quote, parse } from 'shell-quote'; import { quote, parse } from 'shell-quote';
import { USER_SETTINGS_DIR } 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 { import {
@@ -18,6 +17,7 @@ import {
debugLogger, debugLogger,
FatalSandboxError, FatalSandboxError,
GEMINI_DIR, GEMINI_DIR,
homedir,
} from '@google/gemini-cli-core'; } 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';
@@ -82,7 +82,7 @@ export async function start_sandbox(
'-D', '-D',
`TMP_DIR=${fs.realpathSync(os.tmpdir())}`, `TMP_DIR=${fs.realpathSync(os.tmpdir())}`,
'-D', '-D',
`HOME_DIR=${fs.realpathSync(os.homedir())}`, `HOME_DIR=${fs.realpathSync(homedir())}`,
'-D', '-D',
`CACHE_DIR=${fs.realpathSync((await execAsync('getconf DARWIN_USER_CACHE_DIR')).stdout.trim())}`, `CACHE_DIR=${fs.realpathSync((await execAsync('getconf DARWIN_USER_CACHE_DIR')).stdout.trim())}`,
]; ];
@@ -288,18 +288,23 @@ export async function start_sandbox(
// mount user settings directory inside container, after creating if missing // mount user settings directory inside container, after creating if missing
// 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 userHomeDirOnHost = homedir();
const userSettingsDirInSandbox = getContainerPath( const userSettingsDirInSandbox = getContainerPath(
`/home/node/${GEMINI_DIR}`, `/home/node/${GEMINI_DIR}`,
); );
if (!fs.existsSync(userSettingsDirOnHost)) { if (!fs.existsSync(userHomeDirOnHost)) {
fs.mkdirSync(userSettingsDirOnHost); fs.mkdirSync(userHomeDirOnHost, { recursive: true });
} }
const userSettingsDirOnHost = path.join(userHomeDirOnHost, GEMINI_DIR);
if (!fs.existsSync(userSettingsDirOnHost)) {
fs.mkdirSync(userSettingsDirOnHost, { recursive: true });
}
args.push( args.push(
'--volume', '--volume',
`${userSettingsDirOnHost}:${userSettingsDirInSandbox}`, `${userSettingsDirOnHost}:${userSettingsDirInSandbox}`,
); );
if (userSettingsDirInSandbox !== userSettingsDirOnHost) { if (userSettingsDirInSandbox !== getContainerPath(userSettingsDirOnHost)) {
args.push( args.push(
'--volume', '--volume',
`${userSettingsDirOnHost}:${getContainerPath(userSettingsDirOnHost)}`, `${userSettingsDirOnHost}:${getContainerPath(userSettingsDirOnHost)}`,
@@ -309,8 +314,16 @@ export async function start_sandbox(
// mount os.tmpdir() as os.tmpdir() inside container // mount os.tmpdir() as os.tmpdir() inside container
args.push('--volume', `${os.tmpdir()}:${getContainerPath(os.tmpdir())}`); args.push('--volume', `${os.tmpdir()}:${getContainerPath(os.tmpdir())}`);
// mount homedir() as homedir() inside container
if (userHomeDirOnHost !== os.homedir()) {
args.push(
'--volume',
`${userHomeDirOnHost}:${getContainerPath(userHomeDirOnHost)}`,
);
}
// mount gcloud config directory if it exists // mount gcloud config directory if it exists
const gcloudConfigDir = path.join(os.homedir(), '.config', 'gcloud'); const gcloudConfigDir = path.join(homedir(), '.config', 'gcloud');
if (fs.existsSync(gcloudConfigDir)) { if (fs.existsSync(gcloudConfigDir)) {
args.push( args.push(
'--volume', '--volume',
@@ -585,7 +598,7 @@ export async function start_sandbox(
// necessary on Linux to ensure the user exists within the // necessary on Linux to ensure the user exists within the
// container's /etc/passwd file, which is required by os.userInfo(). // container's /etc/passwd file, which is required by os.userInfo().
const username = 'gemini'; const username = 'gemini';
const homeDir = getContainerPath(os.homedir()); const homeDir = getContainerPath(homedir());
const setupUserCommands = [ const setupUserCommands = [
// Use -f with groupadd to avoid errors if the group already exists. // Use -f with groupadd to avoid errors if the group already exists.
@@ -606,7 +619,7 @@ export async function start_sandbox(
// We still need userFlag for the simpler proxy container, which does not have this issue. // We still need userFlag for the simpler proxy container, which does not have this issue.
userFlag = `--user ${uid}:${gid}`; userFlag = `--user ${uid}:${gid}`;
// When forcing a UID in the sandbox, $HOME can be reset to '/', so we copy $HOME as well. // When forcing a UID in the sandbox, $HOME can be reset to '/', so we copy $HOME as well.
args.push('--env', `HOME=${os.homedir()}`); args.push('--env', `HOME=${homedir()}`);
} }
// push container image name // push container image name
@@ -19,6 +19,15 @@ vi.mock('os', async (importOriginal) => {
}; };
}); });
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: () => os.homedir(),
};
});
describe('getUserStartupWarnings', () => { describe('getUserStartupWarnings', () => {
let testRootDir: string; let testRootDir: string;
let homeDir: string; let homeDir: string;
@@ -5,8 +5,9 @@
*/ */
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import * as os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process';
import { homedir } from '@google/gemini-cli-core';
type WarningCheck = { type WarningCheck = {
id: string; id: string;
@@ -20,7 +21,7 @@ const homeDirectoryCheck: WarningCheck = {
try { try {
const [workspaceRealPath, homeRealPath] = await Promise.all([ const [workspaceRealPath, homeRealPath] = await Promise.all([
fs.realpath(workspaceRoot), fs.realpath(workspaceRoot),
fs.realpath(os.homedir()), fs.realpath(homedir()),
]); ]);
if (workspaceRealPath === homeRealPath) { if (workspaceRealPath === homeRealPath) {
@@ -9,9 +9,8 @@ import { HybridTokenStorage } from '../mcp/token-storage/hybrid-token-storage.js
import { OAUTH_FILE } from '../config/storage.js'; import { OAUTH_FILE } from '../config/storage.js';
import type { OAuthCredentials } from '../mcp/token-storage/types.js'; 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 { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import { GEMINI_DIR } from '../utils/paths.js'; import { GEMINI_DIR, homedir } from '../utils/paths.js';
import { coreEvents } from '../utils/events.js'; import { coreEvents } from '../utils/events.js';
const KEYCHAIN_SERVICE_NAME = 'gemini-cli-oauth'; const KEYCHAIN_SERVICE_NAME = 'gemini-cli-oauth';
@@ -91,7 +90,7 @@ export class OAuthCredentialStorage {
await this.storage.deleteCredentials(MAIN_ACCOUNT_KEY); await this.storage.deleteCredentials(MAIN_ACCOUNT_KEY);
// Also try to remove the old file if it exists // Also try to remove the old file if it exists
const oldFilePath = path.join(os.homedir(), GEMINI_DIR, OAUTH_FILE); const oldFilePath = path.join(homedir(), GEMINI_DIR, OAUTH_FILE);
await fs.rm(oldFilePath, { force: true }).catch(() => {}); await fs.rm(oldFilePath, { force: true }).catch(() => {});
} catch (error: unknown) { } catch (error: unknown) {
coreEvents.emitFeedback( coreEvents.emitFeedback(
@@ -107,7 +106,7 @@ export class OAuthCredentialStorage {
* Migrate credentials from old file-based storage to keychain * Migrate credentials from old file-based storage to keychain
*/ */
private static async migrateFromFileStorage(): Promise<Credentials | null> { private static async migrateFromFileStorage(): Promise<Credentials | null> {
const oldFilePath = path.join(os.homedir(), GEMINI_DIR, OAUTH_FILE); const oldFilePath = path.join(homedir(), GEMINI_DIR, OAUTH_FILE);
let credsJson: string; let credsJson: string;
try { try {
+54 -21
View File
@@ -26,16 +26,24 @@ 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'; import { GEMINI_DIR, homedir as pathsHomedir } from '../utils/paths.js';
import { debugLogger } from '../utils/debugLogger.js'; import { debugLogger } from '../utils/debugLogger.js';
import { writeToStdout } from '../utils/stdio.js'; import { writeToStdout } from '../utils/stdio.js';
import { FatalCancellationError } from '../utils/errors.js'; import { FatalCancellationError } from '../utils/errors.js';
import process from 'node:process'; import process from 'node:process';
vi.mock('os', async (importOriginal) => { vi.mock('node:os', async (importOriginal) => {
const os = await importOriginal<typeof import('os')>(); const actual = await importOriginal<typeof import('node:os')>();
return { return {
...os, ...actual,
homedir: vi.fn(),
};
});
vi.mock('../utils/paths.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../utils/paths.js')>();
return {
...actual,
homedir: vi.fn(), homedir: vi.fn(),
}; };
}); });
@@ -89,6 +97,7 @@ describe('oauth2', () => {
path.join(os.tmpdir(), 'gemini-cli-test-home-'), path.join(os.tmpdir(), 'gemini-cli-test-home-'),
); );
vi.mocked(os.homedir).mockReturnValue(tempHomeDir); vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
vi.mocked(pathsHomedir).mockReturnValue(tempHomeDir);
}); });
afterEach(() => { afterEach(() => {
fs.rmSync(tempHomeDir, { recursive: true, force: true }); fs.rmSync(tempHomeDir, { recursive: true, force: true });
@@ -1129,15 +1138,10 @@ describe('oauth2', () => {
() => mockHttpServer as unknown as http.Server, () => mockHttpServer as unknown as http.Server,
); );
// Mock process.on to immediately trigger SIGINT // Mock process.on to capture SIGINT handler
const processOnSpy = vi const processOnSpy = vi
.spyOn(process, 'on') .spyOn(process, 'on')
.mockImplementation((event, listener: () => void) => { .mockImplementation(() => process);
if (event === 'SIGINT') {
listener();
}
return process;
});
const processRemoveListenerSpy = vi.spyOn(process, 'removeListener'); const processRemoveListenerSpy = vi.spyOn(process, 'removeListener');
@@ -1146,6 +1150,24 @@ describe('oauth2', () => {
mockConfig, mockConfig,
); );
// Wait for the SIGINT handler to be registered
let sigIntHandler: (() => void) | undefined;
await vi.waitFor(() => {
const sigintCall = processOnSpy.mock.calls.find(
(call) => call[0] === 'SIGINT',
);
sigIntHandler = sigintCall?.[1] as (() => void) | undefined;
if (!sigIntHandler)
throw new Error('SIGINT handler not registered yet');
});
expect(sigIntHandler).toBeDefined();
// Trigger SIGINT
if (sigIntHandler) {
sigIntHandler();
}
await expect(clientPromise).rejects.toThrow(FatalCancellationError); await expect(clientPromise).rejects.toThrow(FatalCancellationError);
expect(processRemoveListenerSpy).toHaveBeenCalledWith( expect(processRemoveListenerSpy).toHaveBeenCalledWith(
'SIGINT', 'SIGINT',
@@ -1180,17 +1202,10 @@ describe('oauth2', () => {
() => mockHttpServer as unknown as http.Server, () => mockHttpServer as unknown as http.Server,
); );
// Spy on process.stdin.on and immediately trigger Ctrl+C // Spy on process.stdin.on to capture data handler
const stdinOnSpy = vi const stdinOnSpy = vi
.spyOn(process.stdin, 'on') .spyOn(process.stdin, 'on')
.mockImplementation( .mockImplementation(() => process.stdin);
(event: string, listener: (data: Buffer) => void) => {
if (event === 'data') {
listener(Buffer.from([0x03]));
}
return process.stdin;
},
);
const stdinRemoveListenerSpy = vi.spyOn( const stdinRemoveListenerSpy = vi.spyOn(
process.stdin, process.stdin,
@@ -1202,6 +1217,23 @@ describe('oauth2', () => {
mockConfig, mockConfig,
); );
// Wait for the stdin handler to be registered
let dataHandler: ((data: Buffer) => void) | undefined;
await vi.waitFor(() => {
const dataCall = stdinOnSpy.mock.calls.find(
(call: [string, ...unknown[]]) => call[0] === 'data',
);
dataHandler = dataCall?.[1] as ((data: Buffer) => void) | undefined;
if (!dataHandler) throw new Error('stdin handler not registered yet');
});
expect(dataHandler).toBeDefined();
// Trigger Ctrl+C
if (dataHandler) {
dataHandler(Buffer.from([0x03]));
}
await expect(clientPromise).rejects.toThrow(FatalCancellationError); await expect(clientPromise).rejects.toThrow(FatalCancellationError);
expect(stdinRemoveListenerSpy).toHaveBeenCalledWith( expect(stdinRemoveListenerSpy).toHaveBeenCalledWith(
'data', 'data',
@@ -1302,7 +1334,8 @@ describe('oauth2', () => {
tempHomeDir = fs.mkdtempSync( tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-'), path.join(os.tmpdir(), 'gemini-cli-test-home-'),
); );
(os.homedir as Mock).mockReturnValue(tempHomeDir); vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
vi.mocked(pathsHomedir).mockReturnValue(tempHomeDir);
}); });
afterEach(() => { afterEach(() => {
+1 -1
View File
@@ -336,7 +336,7 @@ async function initOauthClient(
// Note that SIGINT might not get raised on Ctrl+C in raw mode // Note that SIGINT might not get raised on Ctrl+C in raw mode
// so we also need to look for Ctrl+C directly in stdin. // so we also need to look for Ctrl+C directly in stdin.
stdinHandler = (data) => { stdinHandler = (data: Buffer) => {
if (data.includes(0x03)) { if (data.includes(0x03)) {
reject( reject(
new FatalCancellationError('Authentication cancelled by user.'), new FatalCancellationError('Authentication cancelled by user.'),
+2 -2
View File
@@ -8,7 +8,7 @@ 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'; import { GEMINI_DIR, homedir } from '../utils/paths.js';
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';
@@ -23,7 +23,7 @@ export class Storage {
} }
static getGlobalGeminiDir(): string { static getGlobalGeminiDir(): string {
const homeDir = os.homedir(); const homeDir = homedir();
if (!homeDir) { if (!homeDir) {
return path.join(os.tmpdir(), GEMINI_DIR); return path.join(os.tmpdir(), GEMINI_DIR);
} }
+2 -3
View File
@@ -6,7 +6,6 @@
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os';
import { import {
EDIT_TOOL_NAME, EDIT_TOOL_NAME,
GLOB_TOOL_NAME, GLOB_TOOL_NAME,
@@ -23,7 +22,7 @@ import process from 'node:process';
import { isGitRepository } from '../utils/gitUtils.js'; import { isGitRepository } from '../utils/gitUtils.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'; import { GEMINI_DIR, homedir } from '../utils/paths.js';
import { debugLogger } from '../utils/debugLogger.js'; import { debugLogger } from '../utils/debugLogger.js';
import { WriteTodosTool } from '../tools/write-todos.js'; import { WriteTodosTool } from '../tools/write-todos.js';
import { resolveModel, isPreviewModel } from '../config/models.js'; import { resolveModel, isPreviewModel } from '../config/models.js';
@@ -53,7 +52,7 @@ export function resolvePathFromEnv(envVar?: string): {
// Safely expand the tilde (~) character to the user's home directory. // Safely expand the tilde (~) character to the user's home directory.
if (customPath.startsWith('~/') || customPath === '~') { if (customPath.startsWith('~/') || customPath === '~') {
try { try {
const home = os.homedir(); // This is the call that can throw an error. const home = homedir(); // This is the call that can throw an error.
if (customPath === '~') { if (customPath === '~') {
customPath = home; customPath = home;
} else { } else {
+11 -2
View File
@@ -14,8 +14,15 @@ vi.mock('node:child_process', async (importOriginal) => {
spawnSync: vi.fn(() => ({ status: 0 })), spawnSync: vi.fn(() => ({ status: 0 })),
}; };
}); });
vi.mock('fs'); vi.mock('node:fs');
vi.mock('os'); vi.mock('node:os');
vi.mock('../utils/paths.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../utils/paths.js')>();
return {
...actual,
homedir: vi.fn(),
};
});
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { getIdeInstaller } from './ide-installer.js'; import { getIdeInstaller } from './ide-installer.js';
@@ -24,12 +31,14 @@ import * as fs from 'node:fs';
import * as os from 'node:os'; import * as os from 'node:os';
import * as path from 'node:path'; import * as path from 'node:path';
import { IDE_DEFINITIONS, type IdeInfo } from './detect-ide.js'; import { IDE_DEFINITIONS, type IdeInfo } from './detect-ide.js';
import { homedir as pathsHomedir } from '../utils/paths.js';
describe('ide-installer', () => { describe('ide-installer', () => {
const HOME_DIR = '/home/user'; const HOME_DIR = '/home/user';
beforeEach(() => { beforeEach(() => {
vi.spyOn(os, 'homedir').mockReturnValue(HOME_DIR); vi.spyOn(os, 'homedir').mockReturnValue(HOME_DIR);
vi.mocked(pathsHomedir).mockReturnValue(HOME_DIR);
}); });
afterEach(() => { afterEach(() => {
+2 -2
View File
@@ -8,9 +8,9 @@ import * as child_process from 'node:child_process';
import * as process from 'node:process'; import * as process from 'node:process';
import * as path from 'node:path'; import * as path from 'node:path';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as os from 'node:os';
import { IDE_DEFINITIONS, type IdeInfo } from './detect-ide.js'; import { IDE_DEFINITIONS, type IdeInfo } from './detect-ide.js';
import { GEMINI_CLI_COMPANION_EXTENSION_NAME } from './constants.js'; import { GEMINI_CLI_COMPANION_EXTENSION_NAME } from './constants.js';
import { homedir } from '../utils/paths.js';
export interface IdeInstaller { export interface IdeInstaller {
install(): Promise<InstallResult>; install(): Promise<InstallResult>;
@@ -49,7 +49,7 @@ async function findCommand(
// 2. Check common installation locations. // 2. Check common installation locations.
const locations: string[] = []; const locations: string[] = [];
const homeDir = os.homedir(); const homeDir = homedir();
if (command === 'code' || command === 'code.cmd') { if (command === 'code' || command === 'code.cmd') {
if (platform === 'darwin') { if (platform === 'darwin') {
+1
View File
@@ -50,6 +50,7 @@ export * from './code_assist/telemetry.js';
export * from './core/apiKeyCredentialStorage.js'; export * from './core/apiKeyCredentialStorage.js';
// Export utilities // Export utilities
export { homedir, tmpdir } from './utils/paths.js';
export * from './utils/paths.js'; export * from './utils/paths.js';
export * from './utils/schemaValidator.js'; export * from './utils/schemaValidator.js';
export * from './utils/errors.js'; export * from './utils/errors.js';
@@ -10,7 +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'; import { GEMINI_DIR, homedir } from '../../utils/paths.js';
export class FileTokenStorage extends BaseTokenStorage { export class FileTokenStorage extends BaseTokenStorage {
private readonly tokenFilePath: string; private readonly tokenFilePath: string;
@@ -18,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_DIR); const configDir = path.join(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();
} }
+15 -2
View File
@@ -18,7 +18,11 @@ import { Storage } from '../config/storage.js';
import * as path from 'node:path'; import * as path from 'node:path';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as os from 'node:os'; import * as os from 'node:os';
import { getProjectHash, GEMINI_DIR } from '../utils/paths.js'; import {
getProjectHash,
GEMINI_DIR,
homedir as pathsHomedir,
} from '../utils/paths.js';
import { spawnAsync } from '../utils/shell-utils.js'; import { spawnAsync } from '../utils/shell-utils.js';
vi.mock('../utils/shell-utils.js', () => ({ vi.mock('../utils/shell-utils.js', () => ({
@@ -52,7 +56,7 @@ vi.mock('../utils/gitUtils.js', () => ({
})); }));
const hoistedMockHomedir = vi.hoisted(() => vi.fn()); const hoistedMockHomedir = vi.hoisted(() => vi.fn());
vi.mock('os', async (importOriginal) => { vi.mock('node:os', async (importOriginal) => {
const actual = await importOriginal<typeof os>(); const actual = await importOriginal<typeof os>();
return { return {
...actual, ...actual,
@@ -60,6 +64,14 @@ vi.mock('os', async (importOriginal) => {
}; };
}); });
vi.mock('../utils/paths.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../utils/paths.js')>();
return {
...actual,
homedir: vi.fn(),
};
});
const hoistedMockDebugLogger = vi.hoisted(() => ({ const hoistedMockDebugLogger = vi.hoisted(() => ({
debug: vi.fn(), debug: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
@@ -93,6 +105,7 @@ describe('GitService', () => {
}); });
hoistedMockHomedir.mockReturnValue(homedir); hoistedMockHomedir.mockReturnValue(homedir);
(pathsHomedir as Mock).mockReturnValue(homedir);
hoistedMockEnv.mockImplementation(() => ({ hoistedMockEnv.mockImplementation(() => ({
checkIsRepo: hoistedMockCheckIsRepo, checkIsRepo: hoistedMockCheckIsRepo,
@@ -11,7 +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'; import { GEMINI_DIR, homedir as pathsHomedir } from './paths.js';
import { debugLogger } from './debugLogger.js'; import { debugLogger } from './debugLogger.js';
vi.mock('node:fs', async (importOriginal) => { vi.mock('node:fs', async (importOriginal) => {
@@ -23,22 +23,30 @@ vi.mock('node:fs', async (importOriginal) => {
} as typeof actual; } as typeof actual;
}); });
vi.mock('os', async (importOriginal) => { vi.mock('node:os', async (importOriginal) => {
const os = await importOriginal<typeof import('os')>(); const os = await importOriginal<typeof import('node:os')>();
return { return {
...os, ...os,
homedir: vi.fn(), homedir: vi.fn(),
}; };
}); });
vi.mock('crypto', async (importOriginal) => { vi.mock('node:crypto', async (importOriginal) => {
const crypto = await importOriginal<typeof import('crypto')>(); const crypto = await importOriginal<typeof import('node:crypto')>();
return { return {
...crypto, ...crypto,
randomUUID: vi.fn(), randomUUID: vi.fn(),
}; };
}); });
vi.mock('./paths.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./paths.js')>();
return {
...actual,
homedir: vi.fn(),
};
});
describe('InstallationManager', () => { describe('InstallationManager', () => {
let tempHomeDir: string; let tempHomeDir: string;
let installationManager: InstallationManager; let installationManager: InstallationManager;
@@ -49,6 +57,7 @@ describe('InstallationManager', () => {
tempHomeDir = fs.mkdtempSync( tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-'), path.join(os.tmpdir(), 'gemini-cli-test-home-'),
); );
(pathsHomedir as Mock).mockReturnValue(tempHomeDir);
(os.homedir as Mock).mockReturnValue(tempHomeDir); (os.homedir as Mock).mockReturnValue(tempHomeDir);
installationManager = new InstallationManager(); installationManager = new InstallationManager();
}); });
@@ -35,6 +35,16 @@ vi.mock('os', async (importOriginal) => {
}; };
}); });
vi.mock('../utils/paths.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../utils/paths.js')>();
return {
...actual,
homedir: vi.fn(),
};
});
import { homedir as pathsHomedir } from './paths.js';
describe('memoryDiscovery', () => { describe('memoryDiscovery', () => {
const DEFAULT_FOLDER_TRUST = true; const DEFAULT_FOLDER_TRUST = true;
let testRootDir: string; let testRootDir: string;
@@ -67,6 +77,7 @@ describe('memoryDiscovery', () => {
cwd = await createEmptyDir(path.join(projectRoot, 'src')); cwd = await createEmptyDir(path.join(projectRoot, 'src'));
homedir = await createEmptyDir(path.join(testRootDir, 'userhome')); homedir = await createEmptyDir(path.join(testRootDir, 'userhome'));
vi.mocked(os.homedir).mockReturnValue(homedir); vi.mocked(os.homedir).mockReturnValue(homedir);
vi.mocked(pathsHomedir).mockReturnValue(homedir);
}); });
afterEach(async () => { afterEach(async () => {
+1 -2
View File
@@ -7,14 +7,13 @@
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as fsSync from 'node:fs'; import * as fsSync from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { homedir } from 'node:os';
import { bfsFileSearch } from './bfsFileSearch.js'; import { bfsFileSearch } from './bfsFileSearch.js';
import { getAllGeminiMdFilenames } from '../tools/memoryTool.js'; import { getAllGeminiMdFilenames } from '../tools/memoryTool.js';
import type { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import type { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { processImports } from './memoryImportProcessor.js'; import { processImports } from './memoryImportProcessor.js';
import type { FileFilteringOptions } from '../config/constants.js'; import type { FileFilteringOptions } from '../config/constants.js';
import { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS } from '../config/constants.js'; import { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS } from '../config/constants.js';
import { GEMINI_DIR } from './paths.js'; import { GEMINI_DIR, homedir } from './paths.js';
import type { ExtensionLoader } from './extensionLoader.js'; import type { ExtensionLoader } from './extensionLoader.js';
import { debugLogger } from './debugLogger.js'; import { debugLogger } from './debugLogger.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
+22 -1
View File
@@ -6,6 +6,7 @@
import path from 'node:path'; import path from 'node:path';
import os from 'node:os'; import os from 'node:os';
import process from 'node:process';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
export const GEMINI_DIR = '.gemini'; export const GEMINI_DIR = '.gemini';
@@ -18,13 +19,33 @@ export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json';
*/ */
export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~]/; export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~]/;
/**
* Returns the home directory.
* If GEMINI_CLI_HOME environment variable is set, it returns its value.
* Otherwise, it returns the user's home directory.
*/
export function homedir(): string {
const envHome = process.env['GEMINI_CLI_HOME'];
if (envHome) {
return envHome;
}
return os.homedir();
}
/**
* Returns the operating system's default directory for temporary files.
*/
export function tmpdir(): string {
return os.tmpdir();
}
/** /**
* Replaces the home directory with a tilde. * Replaces the home directory with a tilde.
* @param path - The path to tildeify. * @param path - The path to tildeify.
* @returns The tildeified path. * @returns The tildeified path.
*/ */
export function tildeifyPath(path: string): string { export function tildeifyPath(path: string): string {
const homeDir = os.homedir(); const homeDir = homedir();
if (path.startsWith(homeDir)) { if (path.startsWith(homeDir)) {
return path.replace(homeDir, '~'); return path.replace(homeDir, '~');
} }
@@ -10,13 +10,13 @@ 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'; import { GEMINI_DIR, homedir as pathsHomedir } from './paths.js';
import { debugLogger } from './debugLogger.js'; import { debugLogger } from './debugLogger.js';
vi.mock('os', async (importOriginal) => { vi.mock('./paths.js', async (importOriginal) => {
const os = await importOriginal<typeof import('os')>(); const actual = await importOriginal<typeof import('./paths.js')>();
return { return {
...os, ...actual,
homedir: vi.fn(), homedir: vi.fn(),
}; };
}); });
@@ -30,7 +30,7 @@ describe('UserAccountManager', () => {
tempHomeDir = fs.mkdtempSync( tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-'), path.join(os.tmpdir(), 'gemini-cli-test-home-'),
); );
(os.homedir as Mock).mockReturnValue(tempHomeDir); (pathsHomedir as Mock).mockReturnValue(tempHomeDir);
accountsFile = () => accountsFile = () =>
path.join(tempHomeDir, GEMINI_DIR, 'google_accounts.json'); path.join(tempHomeDir, GEMINI_DIR, 'google_accounts.json');
userAccountManager = new UserAccountManager(); userAccountManager = new UserAccountManager();
+1 -1
View File
@@ -53,7 +53,7 @@ async function main() {
/* add to the end of plugins array */ /* add to the end of plugins array */
esbuildProblemMatcherPlugin, esbuildProblemMatcherPlugin,
], ],
loader: { '.node': 'file' }, loader: { '.node': 'file', '.wasm': 'binary' },
}); });
if (watch) { if (watch) {
await ctx.watch(); await ctx.watch();
@@ -38,6 +38,15 @@ vi.mock('node:os', async (importOriginal) => {
}; };
}); });
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
tmpdir: vi.fn(() => '/tmp'),
};
});
const vscodeMock = vi.hoisted(() => ({ const vscodeMock = vi.hoisted(() => ({
workspace: { workspace: {
workspaceFolders: [ workspaceFolders: [
@@ -23,7 +23,7 @@ import { randomUUID } from 'node:crypto';
import { type Server as HTTPServer } from 'node:http'; import { type Server as HTTPServer } from 'node:http';
import * as path from 'node:path'; import * as path from 'node:path';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import * as os from 'node:os'; import { tmpdir } from '@google/gemini-cli-core';
import type { z } from 'zod'; import type { z } from 'zod';
import type { DiffManager } from './diff-manager.js'; import type { DiffManager } from './diff-manager.js';
import { OpenFilesManager } from './open-files-manager.js'; import { OpenFilesManager } from './open-files-manager.js';
@@ -343,7 +343,7 @@ export class IDEServer {
this.log(`IDE server listening on http://127.0.0.1:${this.port}`); this.log(`IDE server listening on http://127.0.0.1:${this.port}`);
let portFile: string | undefined; let portFile: string | undefined;
try { try {
const portDir = path.join(os.tmpdir(), 'gemini', 'ide'); const portDir = path.join(tmpdir(), 'gemini', 'ide');
await fs.mkdir(portDir, { recursive: true }); await fs.mkdir(portDir, { recursive: true });
portFile = path.join( portFile = path.join(
portDir, portDir,
+3 -1
View File
@@ -33,10 +33,12 @@ const argv = yargs(hideBin(process.argv)).option('q', {
default: false, default: false,
}).argv; }).argv;
const homedir = () => process.env['GEMINI_CLI_HOME'] || os.homedir();
let geminiSandbox = process.env.GEMINI_SANDBOX; let geminiSandbox = process.env.GEMINI_SANDBOX;
if (!geminiSandbox) { if (!geminiSandbox) {
const userSettingsFile = join(os.homedir(), GEMINI_DIR, 'settings.json'); const userSettingsFile = join(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')),
+4 -1
View File
@@ -24,8 +24,11 @@ const projectHash = crypto
.update(projectRoot) .update(projectRoot)
.digest('hex'); .digest('hex');
// Returns the home directory, respecting GEMINI_CLI_HOME
const homedir = () => process.env['GEMINI_CLI_HOME'] || os.homedir();
// User-level .gemini directory in home // User-level .gemini directory in home
const USER_GEMINI_DIR = path.join(os.homedir(), GEMINI_DIR); const USER_GEMINI_DIR = path.join(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_DIR); const WORKSPACE_GEMINI_DIR = path.join(projectRoot, GEMINI_DIR);