mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat: Add explore subcommand for extension (#11846)
Co-authored-by: christine betts <chrstn@uw.edu>
This commit is contained in:
@@ -9,9 +9,14 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
|
|||||||
import { MessageType } from '../types.js';
|
import { MessageType } from '../types.js';
|
||||||
import { extensionsCommand } from './extensionsCommand.js';
|
import { extensionsCommand } from './extensionsCommand.js';
|
||||||
import { type CommandContext } from './types.js';
|
import { type CommandContext } from './types.js';
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { type ExtensionUpdateAction } from '../state/extensions.js';
|
import { type ExtensionUpdateAction } from '../state/extensions.js';
|
||||||
|
|
||||||
|
import open from 'open';
|
||||||
|
vi.mock('open', () => ({
|
||||||
|
default: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../../config/extensions/update.js', () => ({
|
vi.mock('../../config/extensions/update.js', () => ({
|
||||||
updateExtension: vi.fn(),
|
updateExtension: vi.fn(),
|
||||||
checkForAllExtensionUpdates: vi.fn(),
|
checkForAllExtensionUpdates: vi.fn(),
|
||||||
@@ -26,6 +31,7 @@ describe('extensionsCommand', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
mockGetExtensions.mockReturnValue([]);
|
mockGetExtensions.mockReturnValue([]);
|
||||||
|
vi.mocked(open).mockClear();
|
||||||
mockContext = createMockCommandContext({
|
mockContext = createMockCommandContext({
|
||||||
services: {
|
services: {
|
||||||
config: {
|
config: {
|
||||||
@@ -39,6 +45,11 @@ describe('extensionsCommand', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore any stubbed environment variables, similar to docsCommand.test.ts
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
describe('list', () => {
|
describe('list', () => {
|
||||||
it('should add an EXTENSIONS_LIST item to the UI', async () => {
|
it('should add an EXTENSIONS_LIST item to the UI', async () => {
|
||||||
if (!extensionsCommand.action) throw new Error('Action not defined');
|
if (!extensionsCommand.action) throw new Error('Action not defined');
|
||||||
@@ -302,4 +313,89 @@ describe('extensionsCommand', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('explore', () => {
|
||||||
|
const exploreAction = extensionsCommand.subCommands?.find(
|
||||||
|
(cmd) => cmd.name === 'explore',
|
||||||
|
)?.action;
|
||||||
|
|
||||||
|
if (!exploreAction) {
|
||||||
|
throw new Error('Explore action not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should add an info message and call 'open' in a non-sandbox environment", async () => {
|
||||||
|
// Ensure no special environment variables that would affect behavior
|
||||||
|
vi.stubEnv('NODE_ENV', '');
|
||||||
|
vi.stubEnv('SANDBOX', '');
|
||||||
|
|
||||||
|
await exploreAction(mockContext, '');
|
||||||
|
|
||||||
|
const extensionsUrl = 'https://geminicli.com/extensions/';
|
||||||
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: `Opening extensions page in your browser: ${extensionsUrl}`,
|
||||||
|
},
|
||||||
|
expect.any(Number),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(open).toHaveBeenCalledWith(extensionsUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only add an info message in a sandbox environment', async () => {
|
||||||
|
// Simulate a sandbox environment
|
||||||
|
vi.stubEnv('NODE_ENV', '');
|
||||||
|
vi.stubEnv('SANDBOX', 'gemini-sandbox');
|
||||||
|
const extensionsUrl = 'https://geminicli.com/extensions/';
|
||||||
|
|
||||||
|
await exploreAction(mockContext, '');
|
||||||
|
|
||||||
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: `View available extensions at ${extensionsUrl}`,
|
||||||
|
},
|
||||||
|
expect.any(Number),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure 'open' was not called in the sandbox
|
||||||
|
expect(open).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an info message and not call open in NODE_ENV test environment', async () => {
|
||||||
|
vi.stubEnv('NODE_ENV', 'test');
|
||||||
|
vi.stubEnv('SANDBOX', '');
|
||||||
|
const extensionsUrl = 'https://geminicli.com/extensions/';
|
||||||
|
|
||||||
|
await exploreAction(mockContext, '');
|
||||||
|
|
||||||
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: `Would open extensions page in your browser: ${extensionsUrl} (skipped in test environment)`,
|
||||||
|
},
|
||||||
|
expect.any(Number),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure 'open' was not called in test environment
|
||||||
|
expect(open).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors when opening the browser', async () => {
|
||||||
|
vi.stubEnv('NODE_ENV', '');
|
||||||
|
const extensionsUrl = 'https://geminicli.com/extensions/';
|
||||||
|
const errorMessage = 'Failed to open browser';
|
||||||
|
vi.mocked(open).mockRejectedValue(new Error(errorMessage));
|
||||||
|
|
||||||
|
await exploreAction(mockContext, '');
|
||||||
|
|
||||||
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
type: MessageType.ERROR,
|
||||||
|
text: `Failed to open browser. Check out the extensions gallery at ${extensionsUrl}`,
|
||||||
|
},
|
||||||
|
expect.any(Number),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
type SlashCommand,
|
type SlashCommand,
|
||||||
CommandKind,
|
CommandKind,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
import open from 'open';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
async function listAction(context: CommandContext) {
|
async function listAction(context: CommandContext) {
|
||||||
const historyItem: HistoryItemExtensionsList = {
|
const historyItem: HistoryItemExtensionsList = {
|
||||||
@@ -112,6 +114,51 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
|
|||||||
return updateComplete.then((_) => {});
|
return updateComplete.then((_) => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exploreAction(context: CommandContext) {
|
||||||
|
const extensionsUrl = 'https://geminicli.com/extensions/';
|
||||||
|
|
||||||
|
// Only check for NODE_ENV for explicit test mode, not for unit test framework
|
||||||
|
if (process.env['NODE_ENV'] === 'test') {
|
||||||
|
context.ui.addItem(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: `Would open extensions page in your browser: ${extensionsUrl} (skipped in test environment)`,
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
process.env['SANDBOX'] &&
|
||||||
|
process.env['SANDBOX'] !== 'sandbox-exec'
|
||||||
|
) {
|
||||||
|
context.ui.addItem(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: `View available extensions at ${extensionsUrl}`,
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.ui.addItem(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: `Opening extensions page in your browser: ${extensionsUrl}`,
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await open(extensionsUrl);
|
||||||
|
} catch (_error) {
|
||||||
|
context.ui.addItem(
|
||||||
|
{
|
||||||
|
type: MessageType.ERROR,
|
||||||
|
text: `Failed to open browser. Check out the extensions gallery at ${extensionsUrl}`,
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const listExtensionsCommand: SlashCommand = {
|
const listExtensionsCommand: SlashCommand = {
|
||||||
name: 'list',
|
name: 'list',
|
||||||
description: 'List active extensions',
|
description: 'List active extensions',
|
||||||
@@ -141,11 +188,22 @@ const updateExtensionsCommand: SlashCommand = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exploreExtensionsCommand: SlashCommand = {
|
||||||
|
name: 'explore',
|
||||||
|
description: 'Open extensions page in your browser',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
action: exploreAction,
|
||||||
|
};
|
||||||
|
|
||||||
export const extensionsCommand: SlashCommand = {
|
export const extensionsCommand: SlashCommand = {
|
||||||
name: 'extensions',
|
name: 'extensions',
|
||||||
description: 'Manage extensions',
|
description: 'Manage extensions',
|
||||||
kind: CommandKind.BUILT_IN,
|
kind: CommandKind.BUILT_IN,
|
||||||
subCommands: [listExtensionsCommand, updateExtensionsCommand],
|
subCommands: [
|
||||||
|
listExtensionsCommand,
|
||||||
|
updateExtensionsCommand,
|
||||||
|
exploreExtensionsCommand,
|
||||||
|
],
|
||||||
action: (context, args) =>
|
action: (context, args) =>
|
||||||
// Default to list if no subcommand is provided
|
// Default to list if no subcommand is provided
|
||||||
listExtensionsCommand.action!(context, args),
|
listExtensionsCommand.action!(context, args),
|
||||||
|
|||||||
Reference in New Issue
Block a user