Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,12 @@ Slash commands provide meta-level control over the CLI itself.
- **`nodesc`** or **`nodescriptions`**:
- **Description:** Hide tool descriptions, showing only the tool names.

### `/upgrade`

- **Description:** Open the Gemini Code Assist upgrade page in your browser.
This lets you upgrade your tier for higher usage limits.
- **Note:** This command is only available when logged in with Google.

### `/vim`

- **Description:** Toggle vim mode on or off. When vim mode is enabled, the
Expand Down
35 changes: 35 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ vi.mock('../ui/commands/mcpCommand.js', () => ({
},
}));

vi.mock('../ui/commands/upgradeCommand.js', () => ({
upgradeCommand: {
name: 'upgrade',
description: 'Upgrade command',
kind: 'BUILT_IN',
},
}));

describe('BuiltinCommandLoader', () => {
let mockConfig: Config;

Expand All @@ -141,6 +149,9 @@ describe('BuiltinCommandLoader', () => {
getAllSkills: vi.fn().mockReturnValue([]),
isAdminEnabled: vi.fn().mockReturnValue(true),
}),
getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: 'other',
}),
} as unknown as Config;

restoreCommandMock.mockReturnValue({
Expand All @@ -150,6 +161,27 @@ describe('BuiltinCommandLoader', () => {
});
});

it('should include upgrade command when authType is login_with_google', async () => {
const { AuthType } = await import('@google/gemini-cli-core');
(mockConfig.getContentGeneratorConfig as Mock).mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE,
});
const loader = new BuiltinCommandLoader(mockConfig);
const commands = await loader.loadCommands(new AbortController().signal);
const upgradeCmd = commands.find((c) => c.name === 'upgrade');
expect(upgradeCmd).toBeDefined();
});

it('should exclude upgrade command when authType is NOT login_with_google', async () => {
(mockConfig.getContentGeneratorConfig as Mock).mockReturnValue({
authType: 'other',
});
const loader = new BuiltinCommandLoader(mockConfig);
const commands = await loader.loadCommands(new AbortController().signal);
const upgradeCmd = commands.find((c) => c.name === 'upgrade');
expect(upgradeCmd).toBeUndefined();
});

it('should correctly pass the config object to restore command factory', async () => {
const loader = new BuiltinCommandLoader(mockConfig);
await loader.loadCommands(new AbortController().signal);
Expand Down Expand Up @@ -300,6 +332,9 @@ describe('BuiltinCommandLoader profile', () => {
getAllSkills: vi.fn().mockReturnValue([]),
isAdminEnabled: vi.fn().mockReturnValue(true),
}),
getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: 'other',
}),
} as unknown as Config;
});

Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
isNightly,
startupProfiler,
getAdminErrorMessage,
AuthType,
} from '@google/gemini-cli-core';
import { aboutCommand } from '../ui/commands/aboutCommand.js';
import { agentsCommand } from '../ui/commands/agentsCommand.js';
Expand Down Expand Up @@ -59,6 +60,7 @@ import { shellsCommand } from '../ui/commands/shellsCommand.js';
import { vimCommand } from '../ui/commands/vimCommand.js';
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
import { terminalSetupCommand } from '../ui/commands/terminalSetupCommand.js';
import { upgradeCommand } from '../ui/commands/upgradeCommand.js';

/**
* Loads the core, hard-coded slash commands that are an integral part
Expand Down Expand Up @@ -187,6 +189,10 @@ export class BuiltinCommandLoader implements ICommandLoader {
vimCommand,
setupGithubCommand,
terminalSetupCommand,
...(this.config?.getContentGeneratorConfig()?.authType ===
AuthType.LOGIN_WITH_GOOGLE
? [upgradeCommand]
: []),
];
handle?.end();
return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);
Expand Down
99 changes: 99 additions & 0 deletions packages/cli/src/ui/commands/upgradeCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { upgradeCommand } from './upgradeCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import {
AuthType,
openBrowserSecurely,
UPGRADE_URL_PAGE,
} from '@google/gemini-cli-core';

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
openBrowserSecurely: vi.fn(),
UPGRADE_URL_PAGE: 'https://goo.gle/set-up-gemini-code-assist',
};
});

describe('upgradeCommand', () => {
let mockContext: CommandContext;

beforeEach(() => {
vi.clearAllMocks();
mockContext = createMockCommandContext({
services: {
config: {
getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
},
},
} as unknown as CommandContext);
});

it('should have the correct name and description', () => {
expect(upgradeCommand.name).toBe('upgrade');
expect(upgradeCommand.description).toBe(
'Upgrade your Gemini Code Assist tier for higher limits',
);
});

it('should call openBrowserSecurely with UPGRADE_URL_PAGE when logged in with Google', async () => {
if (!upgradeCommand.action) {
throw new Error('The upgrade command must have an action.');
}

await upgradeCommand.action(mockContext, '');

expect(openBrowserSecurely).toHaveBeenCalledWith(UPGRADE_URL_PAGE);
});

it('should return an error message when NOT logged in with Google', async () => {
vi.mocked(
mockContext.services.config!.getContentGeneratorConfig,
).mockReturnValue({
authType: AuthType.USE_GEMINI,
});

if (!upgradeCommand.action) {
throw new Error('The upgrade command must have an action.');
}

const result = await upgradeCommand.action(mockContext, '');

expect(result).toEqual({
type: 'message',
messageType: 'error',
content:
'The /upgrade command is only available when logged in with Google.',
});
expect(openBrowserSecurely).not.toHaveBeenCalled();
});

it('should return an error message if openBrowserSecurely fails', async () => {
vi.mocked(openBrowserSecurely).mockRejectedValue(
new Error('Failed to open'),
);

if (!upgradeCommand.action) {
throw new Error('The upgrade command must have an action.');
}

const result = await upgradeCommand.action(mockContext, '');

expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Failed to open upgrade page: Failed to open',
});
});
});
50 changes: 50 additions & 0 deletions packages/cli/src/ui/commands/upgradeCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
AuthType,
openBrowserSecurely,
UPGRADE_URL_PAGE,
} from '@google/gemini-cli-core';
import type { SlashCommand } from './types.js';
import { CommandKind } from './types.js';

/**
* Command to open the upgrade page for Gemini Code Assist.
* Only intended to be shown/available when the user is logged in with Google.
*/
export const upgradeCommand: SlashCommand = {
name: 'upgrade',
kind: CommandKind.BUILT_IN,
description: 'Upgrade your Gemini Code Assist tier for higher limits',
autoExecute: true,
action: async (context) => {
const authType =
context.services.config?.getContentGeneratorConfig()?.authType;
if (authType !== AuthType.LOGIN_WITH_GOOGLE) {
// This command should ideally be hidden if not logged in with Google,
// but we add a safety check here just in case.
return {
type: 'message',
messageType: 'error',
content:
'The /upgrade command is only available when logged in with Google.',
};
}

try {
await openBrowserSecurely(UPGRADE_URL_PAGE);
} catch (error) {
return {
type: 'message',
messageType: 'error',
content: `Failed to open upgrade page: ${error instanceof Error ? error.message : String(error)}`,
};
}

return undefined;
},
};
2 changes: 1 addition & 1 deletion packages/core/src/fallback/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
applyAvailabilityTransition,
} from '../availability/policyHelpers.js';

const UPGRADE_URL_PAGE = 'https://goo.gle/set-up-gemini-code-assist';
export const UPGRADE_URL_PAGE = 'https://goo.gle/set-up-gemini-code-assist';

export async function handleFallback(
config: Config,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export * from './scheduler/tool-executor.js';
export * from './core/recordingContentGenerator.js';

export * from './fallback/types.js';
export * from './fallback/handler.js';

export * from './code_assist/codeAssist.js';
export * from './code_assist/oauth2.js';
Expand Down
Loading