summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/services/CommandService.test.ts8
-rw-r--r--packages/cli/src/services/CommandService.ts2
-rw-r--r--packages/cli/src/ui/commands/docsCommand.test.ts99
-rw-r--r--packages/cli/src/ui/commands/docsCommand.ts37
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.ts21
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(),