mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce18470921 | |||
| f8dbc8084a | |||
| 115f8bce8b |
@@ -28,6 +28,7 @@ import {
|
||||
type MergedSettings,
|
||||
createTestMergedSettings,
|
||||
} from './settings.js';
|
||||
import * as SettingsModule from './settings.js';
|
||||
import * as ServerConfig from '@google/gemini-cli-core';
|
||||
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
@@ -813,7 +814,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
});
|
||||
|
||||
it('should pass extension context file paths to loadServerHierarchicalMemory', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
process.argv = ['node', 'script.js', '--prompt', 'test'];
|
||||
const settings = createTestMergedSettings();
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
|
||||
{
|
||||
@@ -862,7 +863,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
});
|
||||
|
||||
it('should pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is true', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
process.argv = ['node', 'script.js', '--prompt', 'test'];
|
||||
const includeDir = path.resolve(path.sep, 'path', 'to', 'include');
|
||||
const settings = createTestMergedSettings({
|
||||
context: {
|
||||
@@ -890,7 +891,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
});
|
||||
|
||||
it('should NOT pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is false', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
process.argv = ['node', 'script.js', '--prompt', 'test'];
|
||||
const settings = createTestMergedSettings({
|
||||
context: {
|
||||
includeDirectories: ['/path/to/include'],
|
||||
@@ -915,6 +916,39 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip extension, memory, and PTY discovery in bootstrap mode', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = await parseArguments(settings);
|
||||
const loadSettingsSpy = vi.spyOn(SettingsModule, 'loadSettings');
|
||||
const getPtySpy = vi.spyOn(ServerConfig, 'getPty');
|
||||
|
||||
await loadCliConfig(settings, 'session-id', argv, {
|
||||
mode: 'bootstrap',
|
||||
loadedSettings: { merged: settings } as SettingsModule.LoadedSettings,
|
||||
});
|
||||
|
||||
expect(loadSettingsSpy).not.toHaveBeenCalled();
|
||||
expect(ExtensionManager.prototype.loadExtensions).not.toHaveBeenCalled();
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||
expect(getPtySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should defer extension and memory discovery during interactive startup', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = await parseArguments(settings);
|
||||
const getPtySpy = vi.spyOn(ServerConfig, 'getPty');
|
||||
|
||||
await loadCliConfig(settings, 'session-id', argv, {
|
||||
loadedSettings: { merged: settings } as SettingsModule.LoadedSettings,
|
||||
});
|
||||
|
||||
expect(ExtensionManager.prototype.loadExtensions).not.toHaveBeenCalled();
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||
expect(getPtySpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeMcpServers', () => {
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
resolveToRealPath,
|
||||
applyAdminAllowlist,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
startupProfiler,
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
import {
|
||||
type Settings,
|
||||
type MergedSettings,
|
||||
type LoadedSettings,
|
||||
saveModelChange,
|
||||
loadSettings,
|
||||
} from './settings.js';
|
||||
@@ -417,6 +419,8 @@ export function isDebugMode(argv: CliArgs): boolean {
|
||||
|
||||
export interface LoadCliConfigOptions {
|
||||
cwd?: string;
|
||||
mode?: 'bootstrap' | 'full';
|
||||
loadedSettings?: LoadedSettings;
|
||||
projectHooks?: { [K in HookEventName]?: HookDefinition[] } & {
|
||||
disabled?: string[];
|
||||
};
|
||||
@@ -428,10 +432,15 @@ export async function loadCliConfig(
|
||||
argv: CliArgs,
|
||||
options: LoadCliConfigOptions = {},
|
||||
): Promise<Config> {
|
||||
const { cwd = process.cwd(), projectHooks } = options;
|
||||
const {
|
||||
cwd = process.cwd(),
|
||||
mode = 'full',
|
||||
loadedSettings,
|
||||
projectHooks,
|
||||
} = options;
|
||||
const debugMode = isDebugMode(argv);
|
||||
|
||||
const loadedSettings = loadSettings(cwd);
|
||||
const resolvedLoadedSettings = loadedSettings ?? loadSettings(cwd);
|
||||
const isBootstrap = mode === 'bootstrap';
|
||||
|
||||
if (argv.sandbox) {
|
||||
process.env['GEMINI_SANDBOX'] = 'true';
|
||||
@@ -439,6 +448,18 @@ export async function loadCliConfig(
|
||||
|
||||
const memoryImportFormat = settings.context?.importFormat || 'tree';
|
||||
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
|
||||
const interactive =
|
||||
!!argv.promptInteractive ||
|
||||
!!argv.acp ||
|
||||
!!argv.experimentalAcp ||
|
||||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
|
||||
!argv.isCommand);
|
||||
const shouldDeferInteractiveStartupWork =
|
||||
!isBootstrap &&
|
||||
interactive &&
|
||||
!argv.listExtensions &&
|
||||
!argv.listSessions &&
|
||||
!argv.deleteSession;
|
||||
|
||||
const ideMode = settings.ide?.enabled ?? false;
|
||||
|
||||
@@ -480,6 +501,7 @@ export async function loadCliConfig(
|
||||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
const clientVersion = await getVersion();
|
||||
const extensionManager = new ExtensionManager({
|
||||
settings,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
@@ -488,9 +510,15 @@ export async function loadCliConfig(
|
||||
enabledExtensionOverrides: argv.extensions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
eventEmitter: coreEvents as EventEmitter<ExtensionEvents>,
|
||||
clientVersion: await getVersion(),
|
||||
clientVersion,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
if (!isBootstrap && !shouldDeferInteractiveStartupWork) {
|
||||
const loadExtensionsHandle = startupProfiler.start(
|
||||
'load_extensions_during_config',
|
||||
);
|
||||
await extensionManager.loadExtensions();
|
||||
loadExtensionsHandle?.end();
|
||||
}
|
||||
|
||||
const extensionPlanSettings = extensionManager
|
||||
.getExtensions()
|
||||
@@ -511,7 +539,12 @@ export async function loadCliConfig(
|
||||
let fileCount = 0;
|
||||
let filePaths: string[] = [];
|
||||
|
||||
if (!experimentalJitContext) {
|
||||
if (
|
||||
!experimentalJitContext &&
|
||||
!isBootstrap &&
|
||||
!shouldDeferInteractiveStartupWork
|
||||
) {
|
||||
const loadMemoryHandle = startupProfiler.start('load_memory');
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
@@ -528,6 +561,7 @@ export async function loadCliConfig(
|
||||
memoryContent = result.memoryContent;
|
||||
fileCount = result.fileCount;
|
||||
filePaths = result.filePaths;
|
||||
loadMemoryHandle?.end();
|
||||
}
|
||||
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
@@ -617,15 +651,6 @@ export async function loadCliConfig(
|
||||
throw err;
|
||||
}
|
||||
|
||||
// -p/--prompt forces non-interactive (headless) mode
|
||||
// -i/--prompt-interactive forces interactive mode with an initial prompt
|
||||
const interactive =
|
||||
!!argv.promptInteractive ||
|
||||
!!argv.acp ||
|
||||
!!argv.experimentalAcp ||
|
||||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
|
||||
!argv.isCommand);
|
||||
|
||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||
|
||||
// In non-interactive mode, exclude tools that require a prompt.
|
||||
@@ -695,7 +720,7 @@ export async function loadCliConfig(
|
||||
? argv.screenReader
|
||||
: (settings.ui?.accessibility?.screenReader ?? false);
|
||||
|
||||
const ptyInfo = await getPty();
|
||||
const ptyInfo = isBootstrap ? null : await getPty();
|
||||
|
||||
const mcpEnabled = settings.admin?.mcp?.enabled ?? true;
|
||||
const extensionsEnabled = settings.admin?.extensions?.enabled ?? true;
|
||||
@@ -741,7 +766,7 @@ export async function loadCliConfig(
|
||||
acpMode: isAcpMode,
|
||||
clientName,
|
||||
sessionId,
|
||||
clientVersion: await getVersion(),
|
||||
clientVersion,
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
sandbox: sandboxConfig,
|
||||
targetDir: cwd,
|
||||
@@ -822,6 +847,8 @@ export async function loadCliConfig(
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
deferInitialMemoryLoad:
|
||||
shouldDeferInteractiveStartupWork && !experimentalJitContext,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
toolOutputMasking: settings.experimental?.toolOutputMasking,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
@@ -864,7 +891,8 @@ export async function loadCliConfig(
|
||||
hooks: settings.hooks || {},
|
||||
disabledHooks: settings.hooksConfig?.disabled || [],
|
||||
projectHooks: projectHooks || {},
|
||||
onModelChange: (model: string) => saveModelChange(loadedSettings, model),
|
||||
onModelChange: (model: string) =>
|
||||
saveModelChange(resolvedLoadedSettings, model),
|
||||
onReload: async () => {
|
||||
const refreshedSettings = loadSettings(cwd);
|
||||
return {
|
||||
|
||||
@@ -94,7 +94,7 @@ interface ExtensionManagerParams {
|
||||
/**
|
||||
* Actual implementation of an ExtensionLoader.
|
||||
*
|
||||
* You must call `loadExtensions` prior to calling other methods on this class.
|
||||
* Extension metadata is loaded lazily via `loadExtensions`/`ensureLoaded`.
|
||||
*/
|
||||
export class ExtensionManager extends ExtensionLoader {
|
||||
private extensionEnablementManager: ExtensionEnablementManager;
|
||||
@@ -146,12 +146,24 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
}
|
||||
|
||||
getExtensions(): GeminiCLIExtension[] {
|
||||
if (!this.loadedExtensions) {
|
||||
throw new Error(
|
||||
'Extensions not yet loaded, must call `loadExtensions` first',
|
||||
);
|
||||
return this.loadedExtensions ?? [];
|
||||
}
|
||||
|
||||
override isLoaded(): boolean {
|
||||
return this.loadedExtensions !== undefined;
|
||||
}
|
||||
|
||||
override async ensureLoaded(): Promise<void> {
|
||||
if (this.loadedExtensions) {
|
||||
return;
|
||||
}
|
||||
return this.loadedExtensions;
|
||||
|
||||
if (this.loadingPromise) {
|
||||
await this.loadingPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadExtensions();
|
||||
}
|
||||
|
||||
async installOrUpdateExtension(
|
||||
|
||||
@@ -289,6 +289,11 @@ export function createTestMergedSettings(
|
||||
) as MergedSettings;
|
||||
}
|
||||
|
||||
export function createDefaultMergedSettings(): MergedSettings {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return getDefaultsFromSchema() as MergedSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* An immutable snapshot of settings state.
|
||||
* Used with useSyncExternalStore for reactive updates.
|
||||
|
||||
@@ -269,6 +269,7 @@ vi.mock('./validateNonInterActiveAuth.js', () => ({
|
||||
|
||||
describe('gemini.tsx main function', () => {
|
||||
let originalIsTTY: boolean | undefined;
|
||||
const originalArgv = [...process.argv];
|
||||
let initialUnhandledRejectionListeners: NodeJS.UnhandledRejectionListener[] =
|
||||
[];
|
||||
|
||||
@@ -284,6 +285,7 @@ describe('gemini.tsx main function', () => {
|
||||
originalIsTTY = process.stdin.isTTY;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(process.stdin as any).isTTY = true;
|
||||
process.argv = ['node', 'script.js'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -296,11 +298,70 @@ describe('gemini.tsx main function', () => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(process.stdin as any).isTTY = originalIsTTY;
|
||||
process.argv = [...originalArgv];
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should fast-path --version without loading settings', async () => {
|
||||
process.argv = ['node', 'script.js', '--version'];
|
||||
|
||||
await main();
|
||||
|
||||
expect(loadSettings).not.toHaveBeenCalled();
|
||||
expect(parseArguments).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fast-path --help without loading settings', async () => {
|
||||
process.argv = ['node', 'script.js', '--help'];
|
||||
|
||||
await main();
|
||||
|
||||
expect(loadSettings).not.toHaveBeenCalled();
|
||||
expect(parseArguments).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not relaunch a child process when sandboxing and memory relaunch are unnecessary', async () => {
|
||||
vi.mocked(loadSandboxConfig).mockResolvedValue(undefined);
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: { advanced: {}, security: { auth: {} }, ui: {} },
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
prompt: 'test',
|
||||
} as unknown as CliArgs);
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(
|
||||
createMockConfig({
|
||||
isInteractive: () => false,
|
||||
getQuestion: () => 'test',
|
||||
getSandbox: () => undefined,
|
||||
}),
|
||||
);
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((code) => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
try {
|
||||
await main();
|
||||
expect.fail('Should have thrown MockProcessExitError');
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(MockProcessExitError);
|
||||
expect((e as MockProcessExitError).code).toBe(0);
|
||||
} finally {
|
||||
processExitSpy.mockRestore();
|
||||
}
|
||||
|
||||
const { relaunchAppInChildProcess } = await import('./utils/relaunch.js');
|
||||
expect(relaunchAppInChildProcess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log unhandled promise rejections and open debug console on first error', async () => {
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
@@ -665,6 +726,42 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
processExitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should use bootstrap config for the pre-relaunch startup pass', async () => {
|
||||
const mockSettings = createMockSettings({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
ui: {},
|
||||
},
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
});
|
||||
vi.mocked(loadSettings).mockReturnValue(mockSettings);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
promptInteractive: false,
|
||||
} as unknown as CliArgs);
|
||||
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(
|
||||
createMockConfig({
|
||||
isInteractive: () => true,
|
||||
getQuestion: () => '',
|
||||
getSandbox: () => undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
await main();
|
||||
|
||||
expect(loadCliConfig).toHaveBeenCalledTimes(2);
|
||||
expect(vi.mocked(loadCliConfig).mock.calls[0][3]).toMatchObject({
|
||||
mode: 'bootstrap',
|
||||
loadedSettings: mockSettings,
|
||||
});
|
||||
expect(vi.mocked(loadCliConfig).mock.calls[1][3]).toMatchObject({
|
||||
loadedSettings: mockSettings,
|
||||
});
|
||||
});
|
||||
|
||||
it('should log warning when theme is not found', async () => {
|
||||
const { themeManager } = await import('./ui/themes/theme-manager.js');
|
||||
const debugLoggerWarnSpy = vi
|
||||
|
||||
+66
-14
@@ -17,6 +17,7 @@ import os from 'node:os';
|
||||
import dns from 'node:dns';
|
||||
import { start_sandbox } from './utils/sandbox.js';
|
||||
import {
|
||||
createDefaultMergedSettings,
|
||||
loadSettings,
|
||||
SettingScope,
|
||||
type DnsResolutionOrder,
|
||||
@@ -119,6 +120,24 @@ import { cleanupBackgroundLogs } from './utils/logCleanup.js';
|
||||
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
|
||||
|
||||
const SLOW_RENDER_MS = 200;
|
||||
const HELP_FLAGS = new Set(['--help', '-h']);
|
||||
const VERSION_FLAGS = new Set(['--version', '-v']);
|
||||
|
||||
function getStartupFastPath(args: string[]): 'help' | 'version' | null {
|
||||
if (args.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.every((arg) => HELP_FLAGS.has(arg))) {
|
||||
return 'help';
|
||||
}
|
||||
|
||||
if (args.every((arg) => VERSION_FLAGS.has(arg))) {
|
||||
return 'version';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateDnsResolutionOrder(
|
||||
order: string | undefined,
|
||||
@@ -341,7 +360,37 @@ export async function startInteractiveUI(
|
||||
registerCleanup(setupTtyCheck());
|
||||
}
|
||||
|
||||
async function runStartupCleanup(
|
||||
config: Config,
|
||||
settings: LoadedSettings,
|
||||
): Promise<void> {
|
||||
const projectTempDir = config.storage.getProjectTempDir();
|
||||
try {
|
||||
await Promise.all([
|
||||
cleanupCheckpoints(projectTempDir),
|
||||
cleanupToolOutputFiles(
|
||||
settings.merged,
|
||||
config.getDebugMode(),
|
||||
projectTempDir,
|
||||
),
|
||||
cleanupBackgroundLogs(),
|
||||
]);
|
||||
} catch (error) {
|
||||
debugLogger.warn('Deferred startup cleanup failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const fastPath = getStartupFastPath(process.argv.slice(2));
|
||||
if (fastPath === 'version') {
|
||||
writeToStdout(`${await getVersion()}\n`);
|
||||
return;
|
||||
}
|
||||
if (fastPath === 'help') {
|
||||
await parseArguments(createDefaultMergedSettings());
|
||||
return;
|
||||
}
|
||||
|
||||
const cliStartupHandle = startupProfiler.start('cli_startup');
|
||||
|
||||
// Listen for admin controls from parent process (IPC) in non-sandbox mode. In
|
||||
@@ -375,6 +424,10 @@ export async function main() {
|
||||
coreEvents.emitFeedback('warning', error.message);
|
||||
});
|
||||
|
||||
const parseArgsHandle = startupProfiler.start('parse_arguments');
|
||||
const argv = await parseArguments(settings.merged);
|
||||
parseArgsHandle?.end();
|
||||
|
||||
const trustedFolders = loadTrustedFolders();
|
||||
trustedFolders.errors.forEach((error: TrustedFoldersError) => {
|
||||
coreEvents.emitFeedback(
|
||||
@@ -382,17 +435,6 @@ export async function main() {
|
||||
`Error in ${error.path}: ${error.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
cleanupCheckpoints(),
|
||||
cleanupToolOutputFiles(settings.merged),
|
||||
cleanupBackgroundLogs(),
|
||||
]);
|
||||
|
||||
const parseArgsHandle = startupProfiler.start('parse_arguments');
|
||||
const argv = await parseArguments(settings.merged);
|
||||
parseArgsHandle?.end();
|
||||
|
||||
if (
|
||||
(argv.allowedTools && argv.allowedTools.length > 0) ||
|
||||
(settings.merged.tools?.allowed && settings.merged.tools.allowed.length > 0)
|
||||
@@ -460,9 +502,13 @@ export async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const bootstrapConfigHandle = startupProfiler.start('load_bootstrap_config');
|
||||
const partialConfig = await loadCliConfig(settings.merged, sessionId, argv, {
|
||||
mode: 'bootstrap',
|
||||
loadedSettings: settings,
|
||||
projectHooks: settings.workspace.settings.hooks,
|
||||
});
|
||||
bootstrapConfigHandle?.end();
|
||||
adminControlsListner.setConfig(partialConfig);
|
||||
|
||||
// Refresh auth to fetch remote admin settings from CCPA and before entering
|
||||
@@ -572,9 +618,7 @@ export async function main() {
|
||||
);
|
||||
await runExitCleanup();
|
||||
process.exit(ExitCodes.SUCCESS);
|
||||
} else {
|
||||
// Relaunch app so we always have a child process that can be internally
|
||||
// restarted if needed.
|
||||
} else if (memoryArgs.length > 0) {
|
||||
await relaunchAppInChildProcess(memoryArgs, [], remoteAdminSettings);
|
||||
}
|
||||
}
|
||||
@@ -585,6 +629,7 @@ export async function main() {
|
||||
{
|
||||
const loadConfigHandle = startupProfiler.start('load_cli_config');
|
||||
const config = await loadCliConfig(settings.merged, sessionId, argv, {
|
||||
loadedSettings: settings,
|
||||
projectHooks: settings.workspace.settings.hooks,
|
||||
});
|
||||
loadConfigHandle?.end();
|
||||
@@ -662,6 +707,13 @@ export async function main() {
|
||||
process.exit(ExitCodes.SUCCESS);
|
||||
}
|
||||
|
||||
const startupCleanup = runStartupCleanup(config, settings);
|
||||
if (config.isInteractive()) {
|
||||
void startupCleanup;
|
||||
} else {
|
||||
await startupCleanup;
|
||||
}
|
||||
|
||||
const wasRaw = process.stdin.isRaw;
|
||||
if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {
|
||||
// Set this as early as possible to avoid spurious characters from
|
||||
|
||||
@@ -796,7 +796,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
Logging in with Google... Restarting Gemini CLI to continue.
|
||||
----------------------------------------------------------------
|
||||
`);
|
||||
await relaunchApp();
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
}
|
||||
}
|
||||
setAuthState(AuthState.Authenticated);
|
||||
@@ -2478,16 +2478,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
onHintClear: () => {},
|
||||
onHintSubmit: () => {},
|
||||
handleRestart: async () => {
|
||||
if (process.send) {
|
||||
const remoteSettings = config.getRemoteAdminSettings();
|
||||
if (remoteSettings) {
|
||||
process.send({
|
||||
type: 'admin-settings-update',
|
||||
settings: remoteSettings,
|
||||
});
|
||||
}
|
||||
}
|
||||
await relaunchApp();
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
},
|
||||
handleNewAgentsSelect: async (choice: NewAgentsChoice) => {
|
||||
if (newAgents && choice === NewAgentsChoice.ACKNOWLEDGE) {
|
||||
|
||||
@@ -22,9 +22,8 @@ import { AuthState } from '../types.js';
|
||||
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { validateAuthMethodWithSettings } from './useAuth.js';
|
||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||
import { Text } from 'ink';
|
||||
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
||||
import { relaunchApp } from '../../utils/processUtils.js';
|
||||
|
||||
// Mocks
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
@@ -36,14 +35,14 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../utils/cleanup.js', () => ({
|
||||
runExitCleanup: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./useAuth.js', () => ({
|
||||
validateAuthMethodWithSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/processUtils.js', () => ({
|
||||
relaunchApp: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
}));
|
||||
@@ -64,7 +63,7 @@ vi.mock('../components/shared/RadioButtonSelect.js', () => ({
|
||||
const mockedUseKeypress = useKeypress as Mock;
|
||||
const mockedRadioButtonSelect = RadioButtonSelect as Mock;
|
||||
const mockedValidateAuthMethod = validateAuthMethodWithSettings as Mock;
|
||||
const mockedRunExitCleanup = runExitCleanup as Mock;
|
||||
const mockedRelaunchApp = relaunchApp as Mock;
|
||||
|
||||
describe('AuthDialog', () => {
|
||||
let props: {
|
||||
@@ -85,6 +84,7 @@ describe('AuthDialog', () => {
|
||||
props = {
|
||||
config: {
|
||||
isBrowserLaunchSuppressed: vi.fn().mockReturnValue(false),
|
||||
getRemoteAdminSettings: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config,
|
||||
settings: {
|
||||
merged: {
|
||||
@@ -351,11 +351,8 @@ describe('AuthDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('exits process for Sign in with Google when browser is suppressed', async () => {
|
||||
it('restarts for Sign in with Google when browser is suppressed', async () => {
|
||||
vi.useFakeTimers();
|
||||
const exitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(() => undefined as never);
|
||||
const logSpy = vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
|
||||
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
@@ -371,10 +368,8 @@ describe('AuthDialog', () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(mockedRunExitCleanup).toHaveBeenCalled();
|
||||
expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
|
||||
expect(mockedRelaunchApp).toHaveBeenCalledWith(undefined);
|
||||
|
||||
exitSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
vi.useRealTimers();
|
||||
unmount();
|
||||
|
||||
@@ -132,7 +132,9 @@ export function AuthDialog({
|
||||
config.isBrowserLaunchSuppressed()
|
||||
) {
|
||||
setExiting(true);
|
||||
setTimeout(relaunchApp, 100);
|
||||
setTimeout(async () => {
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,30 +8,23 @@ import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { LoginWithGoogleRestartDialog } from './LoginWithGoogleRestartDialog.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||
import {
|
||||
RELAUNCH_EXIT_CODE,
|
||||
_resetRelaunchStateForTesting,
|
||||
} from '../../utils/processUtils.js';
|
||||
import { type Config } from '@google/gemini-cli-core';
|
||||
import { relaunchApp } from '../../utils/processUtils.js';
|
||||
|
||||
// Mocks
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/cleanup.js', () => ({
|
||||
runExitCleanup: vi.fn(),
|
||||
vi.mock('../../utils/processUtils.js', () => ({
|
||||
relaunchApp: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const mockedUseKeypress = useKeypress as Mock;
|
||||
const mockedRunExitCleanup = runExitCleanup as Mock;
|
||||
const mockedRelaunchApp = relaunchApp as Mock;
|
||||
|
||||
describe('LoginWithGoogleRestartDialog', () => {
|
||||
const onDismiss = vi.fn();
|
||||
const exitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(() => undefined as never);
|
||||
|
||||
const mockConfig = {
|
||||
getRemoteAdminSettings: vi.fn(),
|
||||
@@ -39,9 +32,7 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
exitSpy.mockClear();
|
||||
vi.useRealTimers();
|
||||
_resetRelaunchStateForTesting();
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
@@ -78,36 +69,33 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each(['r', 'R'])(
|
||||
'calls runExitCleanup and process.exit when %s is pressed',
|
||||
async (keyName) => {
|
||||
vi.useFakeTimers();
|
||||
it.each(['r', 'R'])('restarts when %s is pressed', async (keyName) => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
keypressHandler({
|
||||
name: keyName,
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
sequence: keyName,
|
||||
});
|
||||
keypressHandler({
|
||||
name: keyName,
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
sequence: keyName,
|
||||
});
|
||||
|
||||
// Advance timers to trigger the setTimeout callback
|
||||
await vi.runAllTimersAsync();
|
||||
// Advance timers to trigger the setTimeout callback
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockedRunExitCleanup).toHaveBeenCalledTimes(1);
|
||||
expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
|
||||
expect(mockedRelaunchApp).toHaveBeenCalledTimes(1);
|
||||
expect(mockedRelaunchApp).toHaveBeenCalledWith(undefined);
|
||||
|
||||
vi.useRealTimers();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
vi.useRealTimers();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,16 +26,7 @@ export const LoginWithGoogleRestartDialog = ({
|
||||
return true;
|
||||
} else if (key.name === 'r' || key.name === 'R') {
|
||||
setTimeout(async () => {
|
||||
if (process.send) {
|
||||
const remoteSettings = config.getRemoteAdminSettings();
|
||||
if (remoteSettings) {
|
||||
process.send({
|
||||
type: 'admin-settings-update',
|
||||
settings: remoteSettings,
|
||||
});
|
||||
}
|
||||
}
|
||||
await relaunchApp();
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
}, 100);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -231,7 +231,9 @@ export const DialogManager = ({
|
||||
<Box flexDirection="column">
|
||||
<SettingsDialog
|
||||
onSelect={() => uiActions.closeSettingsDialog()}
|
||||
onRestartRequest={relaunchApp}
|
||||
onRestartRequest={async () => {
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
}}
|
||||
availableTerminalHeight={terminalHeight - staticExtraHeight}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -103,6 +103,21 @@ async function drainStdin() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
export async function cleanupCheckpoints(projectTempDir?: string) {
|
||||
let tempDir = projectTempDir;
|
||||
if (!tempDir) {
|
||||
const storage = new Storage(process.cwd());
|
||||
await storage.initialize();
|
||||
tempDir = storage.getProjectTempDir();
|
||||
}
|
||||
const checkpointsDir = join(tempDir, 'checkpoints');
|
||||
try {
|
||||
await fs.rm(checkpointsDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore errors if the directory doesn't exist or fails to delete.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully shuts down the process, ensuring cleanup runs exactly once.
|
||||
* Guards against concurrent shutdown from signals (SIGHUP, SIGTERM, SIGINT)
|
||||
@@ -161,15 +176,3 @@ export function setupTtyCheck(): () => void {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function cleanupCheckpoints() {
|
||||
const storage = new Storage(process.cwd());
|
||||
await storage.initialize();
|
||||
const tempDir = storage.getProjectTempDir();
|
||||
const checkpointsDir = join(tempDir, 'checkpoints');
|
||||
try {
|
||||
await fs.rm(checkpointsDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore errors if the directory doesn't exist or fails to delete.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,28 +11,56 @@ import {
|
||||
_resetRelaunchStateForTesting,
|
||||
} from './processUtils.js';
|
||||
import * as cleanup from './cleanup.js';
|
||||
import * as relaunch from './relaunch.js';
|
||||
import * as handleAutoUpdate from './handleAutoUpdate.js';
|
||||
|
||||
vi.mock('./handleAutoUpdate.js', () => ({
|
||||
waitForUpdateCompletion: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('./relaunch.js', () => ({
|
||||
relaunchAppInChildProcess: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
describe('processUtils', () => {
|
||||
const processExit = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockReturnValue(undefined as never);
|
||||
const runExitCleanup = vi.spyOn(cleanup, 'runExitCleanup');
|
||||
const relaunchAppInChildProcess = vi.spyOn(
|
||||
relaunch,
|
||||
'relaunchAppInChildProcess',
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
_resetRelaunchStateForTesting();
|
||||
});
|
||||
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
afterEach(() => {
|
||||
delete process.env['SANDBOX'];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
delete (process as any).send;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should wait for updates, run cleanup, and exit with the relaunch code', async () => {
|
||||
await relaunchApp();
|
||||
it('should wait for updates, run cleanup, and exit with the relaunch code when a parent wrapper exists', async () => {
|
||||
process.env['SANDBOX'] = 'sandbox-exec';
|
||||
processExit.mockImplementationOnce(() => {
|
||||
throw new Error('PROCESS_EXIT_CALLED');
|
||||
});
|
||||
|
||||
await expect(relaunchApp()).rejects.toThrow('PROCESS_EXIT_CALLED');
|
||||
expect(handleAutoUpdate.waitForUpdateCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(runExitCleanup).toHaveBeenCalledTimes(1);
|
||||
expect(processExit).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
|
||||
expect(relaunchAppInChildProcess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should spawn a child wrapper on demand when no parent wrapper exists', async () => {
|
||||
await relaunchApp();
|
||||
expect(handleAutoUpdate.waitForUpdateCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(runExitCleanup).toHaveBeenCalledTimes(1);
|
||||
expect(relaunchAppInChildProcess).toHaveBeenCalledWith([], [], undefined);
|
||||
expect(processExit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { AdminControlsSettings } from '@google/gemini-cli-core';
|
||||
import { runExitCleanup } from './cleanup.js';
|
||||
import { waitForUpdateCompletion } from './handleAutoUpdate.js';
|
||||
|
||||
@@ -13,7 +14,8 @@ import { waitForUpdateCompletion } from './handleAutoUpdate.js';
|
||||
export const RELAUNCH_EXIT_CODE = 199;
|
||||
|
||||
/**
|
||||
* Exits the process with a special code to signal that the parent process should relaunch it.
|
||||
* Restarts the CLI, either by signaling an existing parent wrapper or by
|
||||
* spawning one on demand.
|
||||
*/
|
||||
let isRelaunching = false;
|
||||
|
||||
@@ -22,10 +24,24 @@ export function _resetRelaunchStateForTesting(): void {
|
||||
isRelaunching = false;
|
||||
}
|
||||
|
||||
export async function relaunchApp(): Promise<void> {
|
||||
export async function relaunchApp(
|
||||
remoteAdminSettings?: AdminControlsSettings,
|
||||
): Promise<void> {
|
||||
if (isRelaunching) return;
|
||||
isRelaunching = true;
|
||||
await waitForUpdateCompletion();
|
||||
await runExitCleanup();
|
||||
process.exit(RELAUNCH_EXIT_CODE);
|
||||
|
||||
if (process.send || process.env['SANDBOX']) {
|
||||
if (process.send && remoteAdminSettings) {
|
||||
process.send({
|
||||
type: 'admin-settings-update',
|
||||
settings: remoteAdminSettings,
|
||||
});
|
||||
}
|
||||
process.exit(RELAUNCH_EXIT_CODE);
|
||||
}
|
||||
|
||||
const { relaunchAppInChildProcess } = await import('./relaunch.js');
|
||||
await relaunchAppInChildProcess([], [], remoteAdminSettings);
|
||||
}
|
||||
|
||||
@@ -614,6 +614,7 @@ export interface ConfigParameters {
|
||||
disabledSkills?: string[];
|
||||
adminSkillsEnabled?: boolean;
|
||||
experimentalJitContext?: boolean;
|
||||
deferInitialMemoryLoad?: boolean;
|
||||
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
|
||||
disableLLMCorrection?: boolean;
|
||||
plan?: boolean;
|
||||
@@ -832,6 +833,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly adminSkillsEnabled: boolean;
|
||||
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly deferInitialMemoryLoad: boolean;
|
||||
private readonly disableLLMCorrection: boolean;
|
||||
private readonly planEnabled: boolean;
|
||||
private readonly trackerEnabled: boolean;
|
||||
@@ -934,6 +936,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
|
||||
this.modelAvailabilityService = new ModelAvailabilityService();
|
||||
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||
this.deferInitialMemoryLoad = params.deferInitialMemoryLoad ?? false;
|
||||
this.modelSteering = params.modelSteering ?? false;
|
||||
this.userHintService = new UserHintService(() =>
|
||||
this.isModelSteeringEnabled(),
|
||||
@@ -1174,6 +1177,23 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.getExtensionLoader().isLoaded()) {
|
||||
const extensionLoadHandle = startupProfiler.start('load_extensions');
|
||||
await this.getExtensionLoader().ensureLoaded();
|
||||
extensionLoadHandle?.end();
|
||||
} else {
|
||||
await this.getExtensionLoader().ensureLoaded();
|
||||
}
|
||||
|
||||
if (this.deferInitialMemoryLoad) {
|
||||
const memoryLoadHandle = startupProfiler.start('load_memory');
|
||||
const { refreshServerHierarchicalMemory } = await import(
|
||||
'../utils/memoryDiscovery.js'
|
||||
);
|
||||
await refreshServerHierarchicalMemory(this);
|
||||
memoryLoadHandle?.end();
|
||||
}
|
||||
|
||||
// Initialize centralized FileDiscoveryService
|
||||
const discoverToolsHandle = startupProfiler.start('discover_tools');
|
||||
this.getFileService();
|
||||
|
||||
@@ -24,6 +24,19 @@ export abstract class ExtensionLoader {
|
||||
|
||||
constructor(private readonly eventEmitter?: EventEmitter<ExtensionEvents>) {}
|
||||
|
||||
isLoaded(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures extension metadata is available before initialization continues.
|
||||
*
|
||||
* Most implementations have nothing to do here, but loaders backed by disk
|
||||
* discovery can override this to defer filesystem work until after first
|
||||
* render.
|
||||
*/
|
||||
async ensureLoaded(): Promise<void> {}
|
||||
|
||||
/**
|
||||
* All currently known extensions, both active and inactive.
|
||||
*/
|
||||
|
||||
@@ -17,25 +17,38 @@ export interface PtyProcess {
|
||||
kill(signal?: string): void;
|
||||
}
|
||||
|
||||
let ptyImplementationPromise: Promise<PtyImplementation> | undefined;
|
||||
|
||||
export const getPty = async (): Promise<PtyImplementation> => {
|
||||
if (process.env['GEMINI_PTY_INFO'] === 'child_process') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const lydell = '@lydell/node-pty';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(lydell);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return { module, name: 'lydell-node-pty' };
|
||||
} catch (_e) {
|
||||
try {
|
||||
const nodePty = 'node-pty';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(nodePty);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return { module, name: 'node-pty' };
|
||||
} catch (_e2) {
|
||||
return null;
|
||||
}
|
||||
if (!ptyImplementationPromise) {
|
||||
ptyImplementationPromise = (async () => {
|
||||
try {
|
||||
const lydell = '@lydell/node-pty';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(lydell);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return { module, name: 'lydell-node-pty' };
|
||||
} catch (_e) {
|
||||
try {
|
||||
const nodePty = 'node-pty';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(nodePty);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return { module, name: 'node-pty' };
|
||||
} catch (_e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return ptyImplementationPromise;
|
||||
};
|
||||
|
||||
/** For testing purposes only. */
|
||||
export function resetPtyCache(): void {
|
||||
ptyImplementationPromise = undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user