diff options
| -rw-r--r-- | packages/cli/src/services/CommandService.test.ts | 8 | ||||
| -rw-r--r-- | packages/cli/src/services/CommandService.ts | 2 | ||||
| -rw-r--r-- | packages/cli/src/ui/commands/docsCommand.test.ts | 99 | ||||
| -rw-r--r-- | packages/cli/src/ui/commands/docsCommand.ts | 37 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.ts | 21 |
5 files changed, 145 insertions, 22 deletions
diff --git a/packages/cli/src/services/CommandService.test.ts b/packages/cli/src/services/CommandService.test.ts index 3bc618a2..5e5e25ae 100644 --- a/packages/cli/src/services/CommandService.test.ts +++ b/packages/cli/src/services/CommandService.test.ts @@ -10,6 +10,7 @@ import { type SlashCommand } from '../ui/commands/types.js'; import { memoryCommand } from '../ui/commands/memoryCommand.js'; import { helpCommand } from '../ui/commands/helpCommand.js'; import { clearCommand } from '../ui/commands/clearCommand.js'; +import { docsCommand } from '../ui/commands/docsCommand.js'; import { chatCommand } from '../ui/commands/chatCommand.js'; import { authCommand } from '../ui/commands/authCommand.js'; import { themeCommand } from '../ui/commands/themeCommand.js'; @@ -30,6 +31,9 @@ vi.mock('../ui/commands/helpCommand.js', () => ({ vi.mock('../ui/commands/clearCommand.js', () => ({ clearCommand: { name: 'clear', description: 'Mock Clear' }, })); +vi.mock('../ui/commands/docsCommand.js', () => ({ + docsCommand: { name: 'docs', description: 'Mock Docs' }, +})); vi.mock('../ui/commands/authCommand.js', () => ({ authCommand: { name: 'auth', description: 'Mock Auth' }, })); @@ -56,7 +60,7 @@ vi.mock('../ui/commands/mcpCommand.js', () => ({ })); describe('CommandService', () => { - const subCommandLen = 12; + const subCommandLen = 13; describe('when using default production loader', () => { let commandService: CommandService; @@ -88,6 +92,7 @@ describe('CommandService', () => { expect(commandNames).toContain('memory'); expect(commandNames).toContain('help'); expect(commandNames).toContain('clear'); + expect(commandNames).toContain('docs'); expect(commandNames).toContain('chat'); expect(commandNames).toContain('theme'); expect(commandNames).toContain('stats'); @@ -127,6 +132,7 @@ describe('CommandService', () => { chatCommand, clearCommand, compressCommand, + docsCommand, extensionsCommand, helpCommand, mcpCommand, diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts index 49e26833..b9a8df1c 100644 --- a/packages/cli/src/services/CommandService.ts +++ b/packages/cli/src/services/CommandService.ts @@ -8,6 +8,7 @@ import { SlashCommand } from '../ui/commands/types.js'; import { memoryCommand } from '../ui/commands/memoryCommand.js'; import { helpCommand } from '../ui/commands/helpCommand.js'; import { clearCommand } from '../ui/commands/clearCommand.js'; +import { docsCommand } from '../ui/commands/docsCommand.js'; import { mcpCommand } from '../ui/commands/mcpCommand.js'; import { authCommand } from '../ui/commands/authCommand.js'; import { themeCommand } from '../ui/commands/themeCommand.js'; @@ -24,6 +25,7 @@ const loadBuiltInCommands = async (): Promise<SlashCommand[]> => [ chatCommand, clearCommand, compressCommand, + docsCommand, extensionsCommand, helpCommand, mcpCommand, diff --git a/packages/cli/src/ui/commands/docsCommand.test.ts b/packages/cli/src/ui/commands/docsCommand.test.ts new file mode 100644 index 00000000..73b7396a --- /dev/null +++ b/packages/cli/src/ui/commands/docsCommand.test.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import open from 'open'; +import { docsCommand } from './docsCommand.js'; +import { type CommandContext } from './types.js'; +import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; +import { MessageType } from '../types.js'; + +// Mock the 'open' library +vi.mock('open', () => ({ + default: vi.fn(), +})); + +describe('docsCommand', () => { + let mockContext: CommandContext; + beforeEach(() => { + // Create a fresh mock context before each test + mockContext = createMockCommandContext(); + // Reset the `open` mock + vi.mocked(open).mockClear(); + }); + + afterEach(() => { + // Restore any stubbed environment variables + vi.unstubAllEnvs(); + }); + + it("should add an info message and call 'open' in a non-sandbox environment", async () => { + if (!docsCommand.action) { + throw new Error('docsCommand must have an action.'); + } + + const docsUrl = 'https://goo.gle/gemini-cli-docs'; + + await docsCommand.action(mockContext, ''); + + expect(mockContext.ui.addItem).toHaveBeenCalledWith( + { + type: MessageType.INFO, + text: `Opening documentation in your browser: ${docsUrl}`, + }, + expect.any(Number), + ); + + expect(open).toHaveBeenCalledWith(docsUrl); + }); + + it('should only add an info message in a sandbox environment', async () => { + if (!docsCommand.action) { + throw new Error('docsCommand must have an action.'); + } + + // Simulate a sandbox environment + process.env.SANDBOX = 'gemini-sandbox'; + const docsUrl = 'https://goo.gle/gemini-cli-docs'; + + await docsCommand.action(mockContext, ''); + + expect(mockContext.ui.addItem).toHaveBeenCalledWith( + { + type: MessageType.INFO, + text: `Please open the following URL in your browser to view the documentation:\n${docsUrl}`, + }, + expect.any(Number), + ); + + // Ensure 'open' was not called in the sandbox + expect(open).not.toHaveBeenCalled(); + }); + + it("should not open browser for 'sandbox-exec'", async () => { + if (!docsCommand.action) { + throw new Error('docsCommand must have an action.'); + } + + // Simulate the specific 'sandbox-exec' environment + process.env.SANDBOX = 'sandbox-exec'; + const docsUrl = 'https://goo.gle/gemini-cli-docs'; + + await docsCommand.action(mockContext, ''); + + // The logic should fall through to the 'else' block + expect(mockContext.ui.addItem).toHaveBeenCalledWith( + { + type: MessageType.INFO, + text: `Opening documentation in your browser: ${docsUrl}`, + }, + expect.any(Number), + ); + + // 'open' should be called in this specific sandbox case + expect(open).toHaveBeenCalledWith(docsUrl); + }); +}); diff --git a/packages/cli/src/ui/commands/docsCommand.ts b/packages/cli/src/ui/commands/docsCommand.ts new file mode 100644 index 00000000..e53a4a80 --- /dev/null +++ b/packages/cli/src/ui/commands/docsCommand.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import open from 'open'; +import process from 'node:process'; +import { type CommandContext, type SlashCommand } from './types.js'; +import { MessageType } from '../types.js'; + +export const docsCommand: SlashCommand = { + name: 'docs', + description: 'open full Gemini CLI documentation in your browser', + action: async (context: CommandContext): Promise<void> => { + const docsUrl = 'https://goo.gle/gemini-cli-docs'; + + if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') { + context.ui.addItem( + { + type: MessageType.INFO, + text: `Please open the following URL in your browser to view the documentation:\n${docsUrl}`, + }, + Date.now(), + ); + } else { + context.ui.addItem( + { + type: MessageType.INFO, + text: `Opening documentation in your browser: ${docsUrl}`, + }, + Date.now(), + ); + await open(docsUrl); + } + }, +}; diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 8355ea19..8fa3f880 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -201,27 +201,6 @@ export const useSlashCommandProcessor = ( const commands: LegacySlashCommand[] = [ // `/help` and `/clear` have been migrated and REMOVED from this list. { - name: 'docs', - description: 'open full Gemini CLI documentation in your browser', - action: async (_mainCommand, _subCommand, _args) => { - const docsUrl = 'https://goo.gle/gemini-cli-docs'; - if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') { - addMessage({ - type: MessageType.INFO, - content: `Please open the following URL in your browser to view the documentation:\n${docsUrl}`, - timestamp: new Date(), - }); - } else { - addMessage({ - type: MessageType.INFO, - content: `Opening documentation in your browser: ${docsUrl}`, - timestamp: new Date(), - }); - await open(docsUrl); - } - }, - }, - { name: 'editor', description: 'set external editor preference', action: (_mainCommand, _subCommand, _args) => openEditorDialog(), |
