mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-11 02:20:48 -07:00
Introduce GEMINI_CLI_HOME for strict test isolation (#15907)
This commit is contained in:
@@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,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),
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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.'),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user