Skip to content

Commit b181e8d

Browse files
authored
feat(cli): implement /upgrade command (google-gemini#21511)
1 parent 89d64ec commit b181e8d

File tree

7 files changed

+198
-1
lines changed

7 files changed

+198
-1
lines changed

docs/reference/commands.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,12 @@ Slash commands provide meta-level control over the CLI itself.
439439
- **`nodesc`** or **`nodescriptions`**:
440440
- **Description:** Hide tool descriptions, showing only the tool names.
441441

442+
### `/upgrade`
443+
444+
- **Description:** Open the Gemini Code Assist upgrade page in your browser.
445+
This lets you upgrade your tier for higher usage limits.
446+
- **Note:** This command is only available when logged in with Google.
447+
442448
### `/vim`
443449

444450
- **Description:** Toggle vim mode on or off. When vim mode is enabled, the

packages/cli/src/services/BuiltinCommandLoader.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ vi.mock('../ui/commands/mcpCommand.js', () => ({
142142
},
143143
}));
144144

145+
vi.mock('../ui/commands/upgradeCommand.js', () => ({
146+
upgradeCommand: {
147+
name: 'upgrade',
148+
description: 'Upgrade command',
149+
kind: 'BUILT_IN',
150+
},
151+
}));
152+
145153
describe('BuiltinCommandLoader', () => {
146154
let mockConfig: Config;
147155

@@ -163,6 +171,9 @@ describe('BuiltinCommandLoader', () => {
163171
getAllSkills: vi.fn().mockReturnValue([]),
164172
isAdminEnabled: vi.fn().mockReturnValue(true),
165173
}),
174+
getContentGeneratorConfig: vi.fn().mockReturnValue({
175+
authType: 'other',
176+
}),
166177
} as unknown as Config;
167178

168179
restoreCommandMock.mockReturnValue({
@@ -172,6 +183,27 @@ describe('BuiltinCommandLoader', () => {
172183
});
173184
});
174185

186+
it('should include upgrade command when authType is login_with_google', async () => {
187+
const { AuthType } = await import('@google/gemini-cli-core');
188+
(mockConfig.getContentGeneratorConfig as Mock).mockReturnValue({
189+
authType: AuthType.LOGIN_WITH_GOOGLE,
190+
});
191+
const loader = new BuiltinCommandLoader(mockConfig);
192+
const commands = await loader.loadCommands(new AbortController().signal);
193+
const upgradeCmd = commands.find((c) => c.name === 'upgrade');
194+
expect(upgradeCmd).toBeDefined();
195+
});
196+
197+
it('should exclude upgrade command when authType is NOT login_with_google', async () => {
198+
(mockConfig.getContentGeneratorConfig as Mock).mockReturnValue({
199+
authType: 'other',
200+
});
201+
const loader = new BuiltinCommandLoader(mockConfig);
202+
const commands = await loader.loadCommands(new AbortController().signal);
203+
const upgradeCmd = commands.find((c) => c.name === 'upgrade');
204+
expect(upgradeCmd).toBeUndefined();
205+
});
206+
175207
it('should correctly pass the config object to restore command factory', async () => {
176208
const loader = new BuiltinCommandLoader(mockConfig);
177209
await loader.loadCommands(new AbortController().signal);
@@ -364,6 +396,9 @@ describe('BuiltinCommandLoader profile', () => {
364396
getAllSkills: vi.fn().mockReturnValue([]),
365397
isAdminEnabled: vi.fn().mockReturnValue(true),
366398
}),
399+
getContentGeneratorConfig: vi.fn().mockReturnValue({
400+
authType: 'other',
401+
}),
367402
} as unknown as Config;
368403
});
369404

packages/cli/src/services/BuiltinCommandLoader.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isNightly,
1717
startupProfiler,
1818
getAdminErrorMessage,
19+
AuthType,
1920
} from '@google/gemini-cli-core';
2021
import { aboutCommand } from '../ui/commands/aboutCommand.js';
2122
import { agentsCommand } from '../ui/commands/agentsCommand.js';
@@ -59,6 +60,7 @@ import { shellsCommand } from '../ui/commands/shellsCommand.js';
5960
import { vimCommand } from '../ui/commands/vimCommand.js';
6061
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
6162
import { terminalSetupCommand } from '../ui/commands/terminalSetupCommand.js';
63+
import { upgradeCommand } from '../ui/commands/upgradeCommand.js';
6264

6365
/**
6466
* Loads the core, hard-coded slash commands that are an integral part
@@ -223,6 +225,10 @@ export class BuiltinCommandLoader implements ICommandLoader {
223225
vimCommand,
224226
setupGithubCommand,
225227
terminalSetupCommand,
228+
...(this.config?.getContentGeneratorConfig()?.authType ===
229+
AuthType.LOGIN_WITH_GOOGLE
230+
? [upgradeCommand]
231+
: []),
226232
];
227233
handle?.end();
228234
return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, beforeEach, vi } from 'vitest';
8+
import { upgradeCommand } from './upgradeCommand.js';
9+
import { type CommandContext } from './types.js';
10+
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
11+
import {
12+
AuthType,
13+
openBrowserSecurely,
14+
UPGRADE_URL_PAGE,
15+
} from '@google/gemini-cli-core';
16+
17+
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
18+
const actual =
19+
await importOriginal<typeof import('@google/gemini-cli-core')>();
20+
return {
21+
...actual,
22+
openBrowserSecurely: vi.fn(),
23+
UPGRADE_URL_PAGE: 'https://goo.gle/set-up-gemini-code-assist',
24+
};
25+
});
26+
27+
describe('upgradeCommand', () => {
28+
let mockContext: CommandContext;
29+
30+
beforeEach(() => {
31+
vi.clearAllMocks();
32+
mockContext = createMockCommandContext({
33+
services: {
34+
config: {
35+
getContentGeneratorConfig: vi.fn().mockReturnValue({
36+
authType: AuthType.LOGIN_WITH_GOOGLE,
37+
}),
38+
},
39+
},
40+
} as unknown as CommandContext);
41+
});
42+
43+
it('should have the correct name and description', () => {
44+
expect(upgradeCommand.name).toBe('upgrade');
45+
expect(upgradeCommand.description).toBe(
46+
'Upgrade your Gemini Code Assist tier for higher limits',
47+
);
48+
});
49+
50+
it('should call openBrowserSecurely with UPGRADE_URL_PAGE when logged in with Google', async () => {
51+
if (!upgradeCommand.action) {
52+
throw new Error('The upgrade command must have an action.');
53+
}
54+
55+
await upgradeCommand.action(mockContext, '');
56+
57+
expect(openBrowserSecurely).toHaveBeenCalledWith(UPGRADE_URL_PAGE);
58+
});
59+
60+
it('should return an error message when NOT logged in with Google', async () => {
61+
vi.mocked(
62+
mockContext.services.config!.getContentGeneratorConfig,
63+
).mockReturnValue({
64+
authType: AuthType.USE_GEMINI,
65+
});
66+
67+
if (!upgradeCommand.action) {
68+
throw new Error('The upgrade command must have an action.');
69+
}
70+
71+
const result = await upgradeCommand.action(mockContext, '');
72+
73+
expect(result).toEqual({
74+
type: 'message',
75+
messageType: 'error',
76+
content:
77+
'The /upgrade command is only available when logged in with Google.',
78+
});
79+
expect(openBrowserSecurely).not.toHaveBeenCalled();
80+
});
81+
82+
it('should return an error message if openBrowserSecurely fails', async () => {
83+
vi.mocked(openBrowserSecurely).mockRejectedValue(
84+
new Error('Failed to open'),
85+
);
86+
87+
if (!upgradeCommand.action) {
88+
throw new Error('The upgrade command must have an action.');
89+
}
90+
91+
const result = await upgradeCommand.action(mockContext, '');
92+
93+
expect(result).toEqual({
94+
type: 'message',
95+
messageType: 'error',
96+
content: 'Failed to open upgrade page: Failed to open',
97+
});
98+
});
99+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
AuthType,
9+
openBrowserSecurely,
10+
UPGRADE_URL_PAGE,
11+
} from '@google/gemini-cli-core';
12+
import type { SlashCommand } from './types.js';
13+
import { CommandKind } from './types.js';
14+
15+
/**
16+
* Command to open the upgrade page for Gemini Code Assist.
17+
* Only intended to be shown/available when the user is logged in with Google.
18+
*/
19+
export const upgradeCommand: SlashCommand = {
20+
name: 'upgrade',
21+
kind: CommandKind.BUILT_IN,
22+
description: 'Upgrade your Gemini Code Assist tier for higher limits',
23+
autoExecute: true,
24+
action: async (context) => {
25+
const authType =
26+
context.services.config?.getContentGeneratorConfig()?.authType;
27+
if (authType !== AuthType.LOGIN_WITH_GOOGLE) {
28+
// This command should ideally be hidden if not logged in with Google,
29+
// but we add a safety check here just in case.
30+
return {
31+
type: 'message',
32+
messageType: 'error',
33+
content:
34+
'The /upgrade command is only available when logged in with Google.',
35+
};
36+
}
37+
38+
try {
39+
await openBrowserSecurely(UPGRADE_URL_PAGE);
40+
} catch (error) {
41+
return {
42+
type: 'message',
43+
messageType: 'error',
44+
content: `Failed to open upgrade page: ${error instanceof Error ? error.message : String(error)}`,
45+
};
46+
}
47+
48+
return undefined;
49+
},
50+
};

packages/core/src/fallback/handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
applyAvailabilityTransition,
1919
} from '../availability/policyHelpers.js';
2020

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

2323
export async function handleFallback(
2424
config: Config,

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export * from './scheduler/tool-executor.js';
4848
export * from './core/recordingContentGenerator.js';
4949

5050
export * from './fallback/types.js';
51+
export * from './fallback/handler.js';
5152

5253
export * from './code_assist/codeAssist.js';
5354
export * from './code_assist/oauth2.js';

0 commit comments

Comments
 (0)