Compare commits

...

5 Commits

Author SHA1 Message Date
Sehoon Shon 8c5f2708cc perf(cli): optimize startup by skipping redundant parent authentication 2026-03-06 01:34:50 -05:00
Pavan Shinde 0833aca64b docs: format release times as HH:MM UTC (#20726)
Co-authored-by: Sam Roberts <158088236+g-samroberts@users.noreply.github.com>
2026-03-06 03:06:38 +00:00
Sehoon Shon 509d4ae0a9 fix(cli): implement --all flag for extensions uninstall (#21319) 2026-03-06 03:02:01 +00:00
gemini-cli-robot 4d310dda68 Changelog for v0.33.0-preview.3 (#21347)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
Co-authored-by: galz10 <galzahavi@google.com>
2026-03-06 02:11:30 +00:00
gemini-cli-robot e7be0d7bcc Changelog for v0.33.0-preview.2 (#21333)
Co-authored-by: gemini-cli-robot <224641728+gemini-cli-robot@users.noreply.github.com>
2026-03-06 00:35:13 +00:00
15 changed files with 280 additions and 138 deletions
+3 -3
View File
@@ -77,7 +77,7 @@ See [Releases](./docs/releases.md) for more details.
### Preview
New preview releases will be published each week at UTC 2359 on Tuesdays. These
New preview releases will be published each week at UTC 23:59 on Tuesdays. These
releases will not have been fully vetted and may contain regressions or other
outstanding issues. Please help us test and install with `preview` tag.
@@ -87,7 +87,7 @@ npm install -g @google/gemini-cli@preview
### Stable
- New stable releases will be published each week at UTC 2000 on Tuesdays, this
- New stable releases will be published each week at UTC 20:00 on Tuesdays, this
will be the full promotion of last week's `preview` release + any bug fixes
and validations. Use `latest` tag.
@@ -97,7 +97,7 @@ npm install -g @google/gemini-cli@latest
### Nightly
- New releases will be published each day at UTC 0000. This will be all changes
- New releases will be published each day at UTC 00:00. This will be all changes
from the main branch as represented at time of release. It should be assumed
there are pending validations and issues. Use `nightly` tag.
+8 -4
View File
@@ -1,6 +1,6 @@
# Preview release: v0.33.0-preview.1
# Preview release: v0.33.0-preview.3
Released: March 04, 2026
Released: March 05, 2026
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
@@ -29,7 +29,11 @@ npm install -g @google/gemini-cli@preview
## What's Changed
- fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch
- fix(patch): cherry-pick 0135b03 to release/v0.33.0-preview.2-pr-21171
[CONFLICTS] by @gemini-cli-robot in
[#21336](https://github.com/google-gemini/gemini-cli/pull/21336)
* fix(patch): cherry-pick 0659ad1 to release/v0.33.0-preview.0-pr-21042 to patch
version v0.33.0-preview.0 and create version 0.33.0-preview.1 by
@gemini-cli-robot in
[#21047](https://github.com/google-gemini/gemini-cli/pull/21047)
@@ -184,4 +188,4 @@ npm install -g @google/gemini-cli@preview
[#20991](https://github.com/google-gemini/gemini-cli/pull/20991)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.32.0-preview.0...v0.33.0-preview.1
https://github.com/google-gemini/gemini-cli/compare/v0.32.0-preview.0...v0.33.0-preview.3
+28 -11
View File
@@ -319,26 +319,43 @@ export class UninstallExtensionCommand implements Command {
};
}
const name = args.join(' ').trim();
if (!name) {
const all = args.includes('--all');
const names = args.filter((a) => !a.startsWith('--')).map((a) => a.trim());
if (!all && names.length === 0) {
return {
name: this.name,
data: `Usage: /extensions uninstall <extension-name>`,
data: `Usage: /extensions uninstall <extension-names...>|--all`,
};
}
try {
await extensionLoader.uninstallExtension(name, false);
let namesToUninstall: string[] = [];
if (all) {
namesToUninstall = extensionLoader.getExtensions().map((ext) => ext.name);
} else {
namesToUninstall = names;
}
if (namesToUninstall.length === 0) {
return {
name: this.name,
data: `Extension "${name}" uninstalled successfully.`,
};
} catch (error) {
return {
name: this.name,
data: `Failed to uninstall extension "${name}": ${getErrorMessage(error)}`,
data: all ? 'No extensions installed.' : 'No extension name provided.',
};
}
const output: string[] = [];
for (const extensionName of namesToUninstall) {
try {
await extensionLoader.uninstallExtension(extensionName, false);
output.push(`Extension "${extensionName}" uninstalled successfully.`);
} catch (error) {
output.push(
`Failed to uninstall extension "${extensionName}": ${getErrorMessage(error)}`,
);
}
}
return { name: this.name, data: output.join('\n') };
}
}
@@ -28,6 +28,7 @@ import { getErrorMessage } from '../../utils/errors.js';
// Hoisted mocks - these survive vi.clearAllMocks()
const mockUninstallExtension = vi.hoisted(() => vi.fn());
const mockLoadExtensions = vi.hoisted(() => vi.fn());
const mockGetExtensions = vi.hoisted(() => vi.fn());
// Mock dependencies with hoisted functions
vi.mock('../../config/extension-manager.js', async (importOriginal) => {
@@ -38,6 +39,7 @@ vi.mock('../../config/extension-manager.js', async (importOriginal) => {
ExtensionManager: vi.fn().mockImplementation(() => ({
uninstallExtension: mockUninstallExtension,
loadExtensions: mockLoadExtensions,
getExtensions: mockGetExtensions,
setRequestConsent: vi.fn(),
setRequestSetting: vi.fn(),
})),
@@ -93,6 +95,7 @@ describe('extensions uninstall command', () => {
afterEach(() => {
mockLoadExtensions.mockClear();
mockUninstallExtension.mockClear();
mockGetExtensions.mockClear();
vi.clearAllMocks();
});
@@ -145,6 +148,41 @@ describe('extensions uninstall command', () => {
mockCwd.mockRestore();
});
it('should uninstall all extensions when --all flag is used', async () => {
mockLoadExtensions.mockResolvedValue(undefined);
mockUninstallExtension.mockResolvedValue(undefined);
mockGetExtensions.mockReturnValue([{ name: 'ext1' }, { name: 'ext2' }]);
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
await handleUninstall({ all: true });
expect(mockUninstallExtension).toHaveBeenCalledTimes(2);
expect(mockUninstallExtension).toHaveBeenCalledWith('ext1', false);
expect(mockUninstallExtension).toHaveBeenCalledWith('ext2', false);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext1" successfully uninstalled.',
);
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "ext2" successfully uninstalled.',
);
mockCwd.mockRestore();
});
it('should log a message if no extensions are installed and --all flag is used', async () => {
mockLoadExtensions.mockResolvedValue(undefined);
mockGetExtensions.mockReturnValue([]);
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
await handleUninstall({ all: true });
expect(mockUninstallExtension).not.toHaveBeenCalled();
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'No extensions currently installed.',
);
mockCwd.mockRestore();
});
it('should report errors for failed uninstalls but continue with others', async () => {
mockLoadExtensions.mockResolvedValue(undefined);
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
@@ -236,13 +274,14 @@ describe('extensions uninstall command', () => {
const command = uninstallCommand;
it('should have correct command and describe', () => {
expect(command.command).toBe('uninstall <names..>');
expect(command.command).toBe('uninstall [names..]');
expect(command.describe).toBe('Uninstalls one or more extensions.');
});
describe('builder', () => {
interface MockYargs {
positional: Mock;
option: Mock;
check: Mock;
}
@@ -250,11 +289,12 @@ describe('extensions uninstall command', () => {
beforeEach(() => {
yargsMock = {
positional: vi.fn().mockReturnThis(),
option: vi.fn().mockReturnThis(),
check: vi.fn().mockReturnThis(),
};
});
it('should configure positional argument', () => {
it('should configure arguments and options', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
@@ -264,18 +304,31 @@ describe('extensions uninstall command', () => {
type: 'string',
array: true,
});
expect(yargsMock.option).toHaveBeenCalledWith('all', {
type: 'boolean',
describe: 'Uninstall all installed extensions.',
default: false,
});
expect(yargsMock.check).toHaveBeenCalled();
});
it('check function should throw for missing names', () => {
it('check function should throw for missing names and no --all flag', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
const checkCallback = yargsMock.check.mock.calls[0][0];
expect(() => checkCallback({ names: [] })).toThrow(
'Please include at least one extension name to uninstall as a positional argument.',
expect(() => checkCallback({ names: [], all: false })).toThrow(
'Please include at least one extension name to uninstall as a positional argument, or use the --all flag.',
);
});
it('check function should pass if --all flag is used even without names', () => {
(command.builder as (yargs: Argv) => Argv)(
yargsMock as unknown as Argv,
);
const checkCallback = yargsMock.check.mock.calls[0][0];
expect(() => checkCallback({ names: [], all: true })).not.toThrow();
});
});
it('handler should call handleUninstall', async () => {
@@ -283,10 +336,17 @@ describe('extensions uninstall command', () => {
mockUninstallExtension.mockResolvedValue(undefined);
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
interface TestArgv {
names: string[];
[key: string]: unknown;
names?: string[];
all?: boolean;
_: string[];
$0: string;
}
const argv: TestArgv = { names: ['my-extension'], _: [], $0: '' };
const argv: TestArgv = {
names: ['my-extension'],
all: false,
_: [],
$0: '',
};
await (command.handler as unknown as (args: TestArgv) => Promise<void>)(
argv,
);
@@ -14,7 +14,8 @@ import { promptForSetting } from '../../config/extensions/extensionSettings.js';
import { exitCli } from '../utils.js';
interface UninstallArgs {
names: string[]; // can be extension names or source URLs.
names?: string[]; // can be extension names or source URLs.
all?: boolean;
}
export async function handleUninstall(args: UninstallArgs) {
@@ -28,8 +29,24 @@ export async function handleUninstall(args: UninstallArgs) {
});
await extensionManager.loadExtensions();
let namesToUninstall: string[] = [];
if (args.all) {
namesToUninstall = extensionManager
.getExtensions()
.map((ext) => ext.name);
} else if (args.names) {
namesToUninstall = [...new Set(args.names)];
}
if (namesToUninstall.length === 0) {
if (args.all) {
debugLogger.log('No extensions currently installed.');
}
return;
}
const errors: Array<{ name: string; error: string }> = [];
for (const name of [...new Set(args.names)]) {
for (const name of namesToUninstall) {
try {
await extensionManager.uninstallExtension(name, false);
debugLogger.log(`Extension "${name}" successfully uninstalled.`);
@@ -51,7 +68,7 @@ export async function handleUninstall(args: UninstallArgs) {
}
export const uninstallCommand: CommandModule = {
command: 'uninstall <names..>',
command: 'uninstall [names..]',
describe: 'Uninstalls one or more extensions.',
builder: (yargs) =>
yargs
@@ -61,10 +78,15 @@ export const uninstallCommand: CommandModule = {
type: 'string',
array: true,
})
.option('all', {
type: 'boolean',
describe: 'Uninstall all installed extensions.',
default: false,
})
.check((argv) => {
if (!argv.names || argv.names.length === 0) {
if (!argv.all && (!argv.names || argv.names.length === 0)) {
throw new Error(
'Please include at least one extension name to uninstall as a positional argument.',
'Please include at least one extension name to uninstall as a positional argument, or use the --all flag.',
);
}
return true;
@@ -72,7 +94,9 @@ export const uninstallCommand: CommandModule = {
handler: async (argv) => {
await handleUninstall({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
names: argv['names'] as string[],
names: argv['names'] as string[] | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
all: argv['all'] as boolean,
});
await exitCli();
},
+4
View File
@@ -25,6 +25,10 @@ export function setDeferredCommand(command: DeferredCommand) {
deferredCommand = command;
}
export function getDeferredCommand(): DeferredCommand | undefined {
return deferredCommand;
}
export async function runDeferredCommand(settings: MergedSettings) {
if (!deferredCommand) {
return;
+41 -2
View File
@@ -43,6 +43,7 @@ import {
debugLogger,
coreEvents,
AuthType,
fetchCachedCredentials,
} from '@google/gemini-cli-core';
import { act } from 'react';
import { type InitializationResult } from './core/initializer.js';
@@ -130,6 +131,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
off: vi.fn(),
drainBacklogs: vi.fn(),
},
fetchCachedCredentials: vi.fn().mockResolvedValue({}),
};
});
@@ -911,12 +913,47 @@ describe('gemini.tsx main function exit codes', () => {
}
});
it('should exit with 41 for auth failure during sandbox setup', async () => {
it('should skip refreshAuth in parent process when relaunch is expected', async () => {
vi.stubEnv('SANDBOX', '');
const refreshAuthSpy = vi.fn();
vi.mocked(loadCliConfig).mockResolvedValue(
createMockConfig({
refreshAuth: refreshAuthSpy,
getRemoteAdminSettings: vi.fn().mockReturnValue(undefined),
isInteractive: vi.fn().mockReturnValue(true),
}),
);
vi.mocked(loadSettings).mockReturnValue(
createMockSettings({
merged: {
security: { auth: { selectedType: 'google', useExternal: false } },
advanced: { autoConfigureMemory: false },
},
}),
);
vi.mocked(parseArguments).mockResolvedValue({} as CliArgs);
// Initial process (no SANDBOX, no NO_RELAUNCH)
delete process.env['GEMINI_CLI_NO_RELAUNCH'];
try {
await main();
} catch (e) {
if (!(e instanceof MockProcessExitError)) throw e;
}
expect(refreshAuthSpy).not.toHaveBeenCalled();
});
it('should exit with 41 for auth failure during sandbox setup when auth IS required', async () => {
vi.stubEnv('SANDBOX', '');
// Force mustAuthNow = true by simulating sandbox entry without credentials
vi.mocked(loadSandboxConfig).mockResolvedValue({
command: 'docker',
image: 'test-image',
});
vi.mocked(fetchCachedCredentials).mockResolvedValue(null);
vi.mocked(loadCliConfig).mockResolvedValue(
createMockConfig({
refreshAuth: vi.fn().mockRejectedValue(new Error('Auth failed')),
@@ -931,7 +968,9 @@ describe('gemini.tsx main function exit codes', () => {
},
}),
);
vi.mocked(parseArguments).mockResolvedValue({} as CliArgs);
vi.mocked(parseArguments).mockResolvedValue({
sandbox: true,
} as unknown as CliArgs);
try {
await main();
+43 -48
View File
@@ -72,7 +72,6 @@ import {
getVersion,
ValidationCancelledError,
ValidationRequiredError,
type AdminControlsSettings,
} from '@google/gemini-cli-core';
import {
initializeApp,
@@ -108,8 +107,9 @@ import { OverflowProvider } from './ui/contexts/OverflowContext.js';
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
import { profiler } from './ui/components/DebugProfiler.js';
import { runDeferredCommand } from './deferred.js';
import { runDeferredCommand, getDeferredCommand } from './deferred.js';
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
import { fetchCachedCredentials } from '@google/gemini-cli-core';
const SLOW_RENDER_MS = 200;
@@ -328,13 +328,6 @@ export async function startInteractiveUI(
export async function main() {
const cliStartupHandle = startupProfiler.start('cli_startup');
// Listen for admin controls from parent process (IPC) in non-sandbox mode. In
// sandbox mode, we re-fetch the admin controls from the server once we enter
// the sandbox.
// TODO: Cache settings in sandbox mode as well.
const adminControlsListner = setupAdminControlsListener();
registerCleanup(adminControlsListner.cleanup);
const cleanupStdio = patchStdio();
registerSyncCleanup(() => {
// This is needed to ensure we don't lose any buffered output.
@@ -446,13 +439,50 @@ export async function main() {
const partialConfig = await loadCliConfig(settings.merged, sessionId, argv, {
projectHooks: settings.workspace.settings.hooks,
});
adminControlsListner.setConfig(partialConfig);
const isEnteringSandbox = !process.env['SANDBOX'] && !!argv.sandbox;
// We relaunch if we are not already in a sandbox and not already in a
// relaunched process. relaunchAppInChildProcess ensures we always have a
// child process that can be internally restarted.
const willRelaunch =
!process.env['SANDBOX'] && !process.env['GEMINI_CLI_NO_RELAUNCH'];
// Determine if we MUST authenticate in the parent process
const hasDeferredCommand = !!getDeferredCommand();
let needsInteractiveSandboxLogin = false;
if (isEnteringSandbox && !settings.merged.security.auth.useExternal) {
const authType =
settings.merged.security.auth.selectedType ||
(process.env['CLOUD_SHELL'] === 'true' ||
process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true'
? AuthType.COMPUTE_ADC
: AuthType.LOGIN_WITH_GOOGLE);
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
const existingCredentials = await fetchCachedCredentials();
if (!existingCredentials) {
// Sandbox environment might block interactive login, so do it here
needsInteractiveSandboxLogin = true;
}
}
}
const mustAuthNow = hasDeferredCommand || needsInteractiveSandboxLogin;
// Refresh auth to fetch remote admin settings from CCPA and before entering
// the sandbox because the sandbox will interfere with the Oauth2 web
// redirect.
//
// We skip this in the parent process if we are about to relaunch, as the
// child process will perform its own authentication and fetch admin settings
// itself.
let initialAuthFailed = false;
if (!settings.merged.security.auth.useExternal) {
if (
!settings.merged.security.auth.useExternal &&
(!willRelaunch || mustAuthNow)
) {
try {
if (
partialConfig.isInteractive() &&
@@ -501,6 +531,7 @@ export async function main() {
}
// Run deferred command now that we have admin settings.
// Note: if auth was skipped, settings.merged.admin will use defaults.
await runDeferredCommand(settings.merged);
// hop into sandbox if we are outside and sandboxing is enabled
@@ -558,7 +589,7 @@ export async function main() {
} else {
// Relaunch app so we always have a child process that can be internally
// restarted if needed.
await relaunchAppInChildProcess(memoryArgs, [], remoteAdminSettings);
await relaunchAppInChildProcess(memoryArgs, []);
}
}
@@ -577,8 +608,6 @@ export async function main() {
// access to the project identifier.
await config.storage.initialize();
adminControlsListner.setConfig(config);
if (config.isInteractive() && settings.merged.general.devtools) {
const { setupInitialActivityLogger } = await import(
'./utils/devtoolsService.js'
@@ -870,37 +899,3 @@ export function initializeOutputListenersAndFlush() {
}
coreEvents.drainBacklogs();
}
function setupAdminControlsListener() {
let pendingSettings: AdminControlsSettings | undefined;
let config: Config | undefined;
const messageHandler = (msg: unknown) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const message = msg as {
type?: string;
settings?: AdminControlsSettings;
};
if (message?.type === 'admin-settings' && message.settings) {
if (config) {
config.setRemoteAdminSettings(message.settings);
} else {
pendingSettings = message.settings;
}
}
};
process.on('message', messageHandler);
return {
setConfig: (newConfig: Config) => {
config = newConfig;
if (pendingSettings) {
config.setRemoteAdminSettings(pendingSettings);
}
},
cleanup: () => {
process.off('message', messageHandler);
},
};
}
+16
View File
@@ -160,6 +160,7 @@ vi.mock('./hooks/useIdeTrustListener.js');
vi.mock('./hooks/useMessageQueue.js');
vi.mock('./hooks/useApprovalModeIndicator.js');
vi.mock('./hooks/useGitBranchName.js');
vi.mock('./hooks/useExtensionUpdates.js');
vi.mock('./contexts/VimModeContext.js');
vi.mock('./contexts/SessionContext.js');
vi.mock('./components/shared/text-buffer.js');
@@ -218,6 +219,10 @@ import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useApprovalModeIndicator } from './hooks/useApprovalModeIndicator.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
import {
useConfirmUpdateRequests,
useExtensionUpdates,
} from './hooks/useExtensionUpdates.js';
import { useVimMode } from './contexts/VimModeContext.js';
import { useSessionStats } from './contexts/SessionContext.js';
import { useTextBuffer } from './components/shared/text-buffer.js';
@@ -299,6 +304,8 @@ describe('AppContainer State Management', () => {
const mockedUseMessageQueue = useMessageQueue as Mock;
const mockedUseApprovalModeIndicator = useApprovalModeIndicator as Mock;
const mockedUseGitBranchName = useGitBranchName as Mock;
const mockedUseConfirmUpdateRequests = useConfirmUpdateRequests as Mock;
const mockedUseExtensionUpdates = useExtensionUpdates as Mock;
const mockedUseVimMode = useVimMode as Mock;
const mockedUseSessionStats = useSessionStats as Mock;
const mockedUseTextBuffer = useTextBuffer as Mock;
@@ -451,6 +458,15 @@ describe('AppContainer State Management', () => {
isFocused: true,
hasReceivedFocusEvent: true,
});
mockedUseConfirmUpdateRequests.mockReturnValue({
addConfirmUpdateExtensionRequest: vi.fn(),
confirmUpdateExtensionRequests: [],
});
mockedUseExtensionUpdates.mockReturnValue({
extensionsUpdateState: new Map(),
extensionsUpdateStateInternal: new Map(),
dispatchExtensionStateUpdate: vi.fn(),
});
// Mock Config
mockConfig = makeFakeConfig();
-9
View File
@@ -2484,15 +2484,6 @@ 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();
},
handleNewAgentsSelect: async (choice: NewAgentsChoice) => {
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { type Config } from '@google/gemini-cli-core';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js';
@@ -12,12 +11,10 @@ import { relaunchApp } from '../../utils/processUtils.js';
interface LoginWithGoogleRestartDialogProps {
onDismiss: () => void;
config: Config;
}
export const LoginWithGoogleRestartDialog = ({
onDismiss,
config,
}: LoginWithGoogleRestartDialogProps) => {
useKeypress(
(key) => {
@@ -26,15 +23,6 @@ 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();
}, 100);
return true;
@@ -755,7 +755,7 @@ describe('extensionsCommand', () => {
await uninstallAction!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.ERROR,
text: 'Usage: /extensions uninstall <extension-name>',
text: 'Usage: /extensions uninstall <extension-names...>|--all',
});
expect(mockUninstallExtension).not.toHaveBeenCalled();
});
@@ -594,33 +594,53 @@ async function uninstallAction(context: CommandContext, args: string) {
return;
}
const name = args.trim();
if (!name) {
const uninstallArgs = args.split(' ').filter((value) => value.length > 0);
const all = uninstallArgs.includes('--all');
const names = uninstallArgs.filter((a) => !a.startsWith('--'));
if (!all && names.length === 0) {
context.ui.addItem({
type: MessageType.ERROR,
text: `Usage: /extensions uninstall <extension-name>`,
text: `Usage: /extensions uninstall <extension-names...>|--all`,
});
return;
}
context.ui.addItem({
type: MessageType.INFO,
text: `Uninstalling extension "${name}"...`,
});
let namesToUninstall: string[] = [];
if (all) {
namesToUninstall = extensionLoader.getExtensions().map((ext) => ext.name);
} else {
namesToUninstall = names;
}
try {
await extensionLoader.uninstallExtension(name, false);
if (namesToUninstall.length === 0) {
context.ui.addItem({
type: MessageType.INFO,
text: `Extension "${name}" uninstalled successfully.`,
text: all ? 'No extensions installed.' : 'No extension name provided.',
});
} catch (error) {
return;
}
for (const extensionName of namesToUninstall) {
context.ui.addItem({
type: MessageType.ERROR,
text: `Failed to uninstall extension "${name}": ${getErrorMessage(
error,
)}`,
type: MessageType.INFO,
text: `Uninstalling extension "${extensionName}"...`,
});
try {
await extensionLoader.uninstallExtension(extensionName, false);
context.ui.addItem({
type: MessageType.INFO,
text: `Extension "${extensionName}" uninstalled successfully.`,
});
} catch (error) {
context.ui.addItem({
type: MessageType.ERROR,
text: `Failed to uninstall extension "${extensionName}": ${getErrorMessage(
error,
)}`,
});
}
}
}
+2 -18
View File
@@ -6,10 +6,7 @@
import { spawn } from 'node:child_process';
import { RELAUNCH_EXIT_CODE } from './processUtils.js';
import {
writeToStderr,
type AdminControlsSettings,
} from '@google/gemini-cli-core';
import { writeToStderr } from '@google/gemini-cli-core';
export async function relaunchOnExitCode(runner: () => Promise<number>) {
while (true) {
@@ -34,14 +31,11 @@ export async function relaunchOnExitCode(runner: () => Promise<number>) {
export async function relaunchAppInChildProcess(
additionalNodeArgs: string[],
additionalScriptArgs: string[],
remoteAdminSettings?: AdminControlsSettings,
) {
if (process.env['GEMINI_CLI_NO_RELAUNCH']) {
return;
}
let latestAdminSettings = remoteAdminSettings;
const runner = () => {
// process.argv is [node, script, ...args]
// We want to construct [ ...nodeArgs, script, ...scriptArgs]
@@ -61,20 +55,10 @@ export async function relaunchAppInChildProcess(
process.stdin.pause();
const child = spawn(process.execPath, nodeArgs, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
stdio: ['inherit', 'inherit', 'inherit'],
env: newEnv,
});
if (latestAdminSettings) {
child.send({ type: 'admin-settings', settings: latestAdminSettings });
}
child.on('message', (msg: { type?: string; settings?: unknown }) => {
if (msg.type === 'admin-settings-update' && msg.settings) {
latestAdminSettings = msg.settings as AdminControlsSettings;
}
});
return new Promise<number>((resolve, reject) => {
child.on('error', reject);
child.on('close', (code) => {
+1 -1
View File
@@ -642,7 +642,7 @@ export function getAvailablePort(): Promise<number> {
});
}
async function fetchCachedCredentials(): Promise<
export async function fetchCachedCredentials(): Promise<
Credentials | JWTInput | null
> {
const useEncryptedStorage = getUseEncryptedStorageFlag();