mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-12 15:10:59 -07:00
feat(cli): integrate profile support into subcommands and fix MCP loading
This commit is contained in:
@@ -9,7 +9,9 @@ import { coreEvents } from '@google/gemini-cli-core';
|
||||
import { handleList, listCommand } from './list.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const { mockCoreDebugLogger } = await import(
|
||||
@@ -25,6 +27,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
|
||||
vi.mock('../../config/extension-manager.js');
|
||||
vi.mock('../../config/settings.js');
|
||||
vi.mock('../../config/config.js');
|
||||
vi.mock('../../utils/errors.js');
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: vi.fn(),
|
||||
@@ -40,6 +43,7 @@ describe('extensions list command', () => {
|
||||
const mockLoadSettings = vi.mocked(loadSettings);
|
||||
const mockGetErrorMessage = vi.mocked(getErrorMessage);
|
||||
const mockExtensionManager = vi.mocked(ExtensionManager);
|
||||
const mockLoadCliConfig = vi.mocked(loadCliConfig);
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
@@ -55,10 +59,17 @@ describe('extensions list command', () => {
|
||||
describe('handleList', () => {
|
||||
it('should log a message if no extensions are installed', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue([]);
|
||||
await handleList();
|
||||
await handleList({} as unknown as CliArgs);
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -69,10 +80,17 @@ describe('extensions list command', () => {
|
||||
|
||||
it('should output empty JSON array if no extensions are installed and output-format is json', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue([]);
|
||||
await handleList({ outputFormat: 'json' });
|
||||
await handleList({} as unknown as CliArgs, { outputFormat: 'json' });
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith('log', '[]');
|
||||
mockCwd.mockRestore();
|
||||
@@ -80,6 +98,14 @@ describe('extensions list command', () => {
|
||||
|
||||
it('should list all installed extensions', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
const extensions = [
|
||||
{ name: 'ext1', version: '1.0.0' },
|
||||
{ name: 'ext2', version: '2.0.0' },
|
||||
@@ -90,7 +116,7 @@ describe('extensions list command', () => {
|
||||
mockExtensionManager.prototype.toOutputString = vi.fn(
|
||||
(ext) => `${ext.name}@${ext.version}`,
|
||||
);
|
||||
await handleList();
|
||||
await handleList({} as unknown as CliArgs);
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -101,6 +127,14 @@ describe('extensions list command', () => {
|
||||
|
||||
it('should list all installed extensions in JSON format', async () => {
|
||||
const mockCwd = vi.spyOn(process, 'cwd').mockReturnValue('/test/dir');
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
const extensions = [
|
||||
{ name: 'ext1', version: '1.0.0' },
|
||||
{ name: 'ext2', version: '2.0.0' },
|
||||
@@ -108,7 +142,7 @@ describe('extensions list command', () => {
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue(extensions);
|
||||
await handleList({ outputFormat: 'json' });
|
||||
await handleList({} as unknown as CliArgs, { outputFormat: 'json' });
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -124,12 +158,11 @@ describe('extensions list command', () => {
|
||||
code?: string | number | null | undefined,
|
||||
) => never);
|
||||
const error = new Error('List failed');
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockRejectedValue(error);
|
||||
mockLoadCliConfig.mockRejectedValue(error);
|
||||
mockGetErrorMessage.mockReturnValue('List failed message');
|
||||
|
||||
await handleList();
|
||||
|
||||
await handleList({} as unknown as CliArgs);
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
@@ -167,6 +200,13 @@ describe('extensions list command', () => {
|
||||
});
|
||||
|
||||
it('handler should call handleList with parsed arguments', async () => {
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi
|
||||
.fn()
|
||||
.mockReturnValue(mockExtensionManager.prototype),
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
mockExtensionManager.prototype.loadExtensions = vi
|
||||
.fn()
|
||||
.mockResolvedValue([]);
|
||||
@@ -177,7 +217,7 @@ describe('extensions list command', () => {
|
||||
)({
|
||||
'output-format': 'json',
|
||||
});
|
||||
expect(mockExtensionManager.prototype.loadExtensions).toHaveBeenCalled();
|
||||
expect(mockLoadCliConfig).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,21 +7,29 @@
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import type { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
|
||||
export async function handleList(options?: { outputFormat?: 'text' | 'json' }) {
|
||||
export async function handleList(
|
||||
argv: CliArgs,
|
||||
options?: { outputFormat?: 'text' | 'json' },
|
||||
) {
|
||||
try {
|
||||
const workspaceDir = process.cwd();
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
settings: loadSettings(workspaceDir).merged,
|
||||
});
|
||||
const settings = loadSettings(workspaceDir);
|
||||
const config = await loadCliConfig(
|
||||
settings.merged,
|
||||
'extensions-list-session',
|
||||
argv,
|
||||
{ cwd: workspaceDir },
|
||||
);
|
||||
|
||||
// Initialize to trigger extension loading (and profile filtering)
|
||||
await config.initialize();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const extensionManager = config.getExtensionLoader() as ExtensionManager;
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
if (extensions.length === 0) {
|
||||
if (options?.outputFormat === 'json') {
|
||||
@@ -61,7 +69,8 @@ export const listCommand: CommandModule = {
|
||||
default: 'text',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await handleList({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
await handleList(argv as unknown as CliArgs, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
outputFormat: argv['output-format'] as 'text' | 'json',
|
||||
});
|
||||
|
||||
@@ -14,30 +14,42 @@ import {
|
||||
import { loadSettings } from '../../config/settings.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { getMcpServersFromConfig } from './list.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import type { ExtensionManager } from '../../config/extension-manager.js';
|
||||
|
||||
const GREEN = '\x1b[32m';
|
||||
const YELLOW = '\x1b[33m';
|
||||
const RED = '\x1b[31m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
interface Args {
|
||||
interface Args extends CliArgs {
|
||||
name: string;
|
||||
session?: boolean;
|
||||
}
|
||||
|
||||
async function handleEnable(args: Args): Promise<void> {
|
||||
async function handleEnable(argv: Args): Promise<void> {
|
||||
const manager = McpServerEnablementManager.getInstance();
|
||||
const name = normalizeServerId(args.name);
|
||||
const name = normalizeServerId(argv.name);
|
||||
|
||||
// Check settings blocks
|
||||
const settings = loadSettings();
|
||||
|
||||
const config = await loadCliConfig(settings.merged, 'mcp-enable', argv, {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
await config.initialize();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const extensionManager = config.getExtensionLoader() as ExtensionManager;
|
||||
|
||||
// Get all servers including extensions
|
||||
const servers = await getMcpServersFromConfig();
|
||||
const { mcpServers: servers } = await getMcpServersFromConfig(
|
||||
settings.merged,
|
||||
extensionManager,
|
||||
);
|
||||
const normalizedServerNames = Object.keys(servers).map(normalizeServerId);
|
||||
if (!normalizedServerNames.includes(name)) {
|
||||
debugLogger.log(
|
||||
`${RED}Error:${RESET} Server '${args.name}' not found. Use 'gemini mcp' to see available servers.`,
|
||||
`${RED}Error:${RESET} Server '${argv.name}' not found. Use 'gemini mcp' to see available servers.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -56,7 +68,7 @@ async function handleEnable(args: Args): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.session) {
|
||||
if (argv.session) {
|
||||
manager.clearSessionDisable(name);
|
||||
debugLogger.log(`${GREEN}✓${RESET} Session disable cleared for '${name}'.`);
|
||||
} else {
|
||||
@@ -71,21 +83,34 @@ async function handleEnable(args: Args): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisable(args: Args): Promise<void> {
|
||||
async function handleDisable(argv: Args): Promise<void> {
|
||||
const manager = McpServerEnablementManager.getInstance();
|
||||
const name = normalizeServerId(args.name);
|
||||
const name = normalizeServerId(argv.name);
|
||||
|
||||
// Check settings blocks
|
||||
const settings = loadSettings();
|
||||
|
||||
const config = await loadCliConfig(settings.merged, 'mcp-disable', argv, {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
await config.initialize();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const extensionManager = config.getExtensionLoader() as ExtensionManager;
|
||||
|
||||
// Get all servers including extensions
|
||||
const servers = await getMcpServersFromConfig();
|
||||
const { mcpServers: servers } = await getMcpServersFromConfig(
|
||||
settings.merged,
|
||||
extensionManager,
|
||||
);
|
||||
const normalizedServerNames = Object.keys(servers).map(normalizeServerId);
|
||||
if (!normalizedServerNames.includes(name)) {
|
||||
debugLogger.log(
|
||||
`${RED}Error:${RESET} Server '${args.name}' not found. Use 'gemini mcp' to see available servers.`,
|
||||
`${RED}Error:${RESET} Server '${argv.name}' not found. Use 'gemini mcp' to see available servers.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.session) {
|
||||
if (argv.session) {
|
||||
manager.disableForSession(name);
|
||||
debugLogger.log(
|
||||
`${GREEN}✓${RESET} MCP server '${name}' disabled for this session.`,
|
||||
@@ -100,6 +125,7 @@ export const enableCommand: CommandModule<object, Args> = {
|
||||
command: 'enable <name>',
|
||||
describe: 'Enable an MCP server',
|
||||
builder: (yargs) =>
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||
yargs
|
||||
.positional('name', {
|
||||
describe: 'MCP server name to enable',
|
||||
@@ -110,7 +136,8 @@ export const enableCommand: CommandModule<object, Args> = {
|
||||
describe: 'Clear session-only disable',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}),
|
||||
}) as any,
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||
handler: async (argv) => {
|
||||
await handleEnable(argv as Args);
|
||||
await exitCli();
|
||||
@@ -121,6 +148,7 @@ export const disableCommand: CommandModule<object, Args> = {
|
||||
command: 'disable <name>',
|
||||
describe: 'Disable an MCP server',
|
||||
builder: (yargs) =>
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||
yargs
|
||||
.positional('name', {
|
||||
describe: 'MCP server name to disable',
|
||||
@@ -131,7 +159,8 @@ export const disableCommand: CommandModule<object, Args> = {
|
||||
describe: 'Disable for current session only',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}),
|
||||
}) as any,
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
|
||||
handler: async (argv) => {
|
||||
await handleDisable(argv as Args);
|
||||
await exitCli();
|
||||
|
||||
@@ -24,6 +24,8 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { ExtensionStorage } from '../../config/extensions/storage.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import { McpServerEnablementManager } from '../../config/mcp/index.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../../config/settings.js', async (importOriginal) => {
|
||||
const actual =
|
||||
@@ -33,6 +35,7 @@ vi.mock('../../config/settings.js', async (importOriginal) => {
|
||||
loadSettings: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock('../../config/config.js');
|
||||
vi.mock('../../config/extensions/storage.js', () => ({
|
||||
ExtensionStorage: {
|
||||
getUserExtensionsDir: vi.fn(),
|
||||
@@ -62,6 +65,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
{
|
||||
getGlobalSettingsPath: () => '/tmp/gemini/settings.json',
|
||||
getGlobalGeminiDir: () => '/tmp/gemini',
|
||||
getGlobalTempDir: () => '/tmp/gemini/tmp',
|
||||
},
|
||||
),
|
||||
GEMINI_DIR: '.gemini',
|
||||
@@ -138,7 +142,13 @@ describe('mcp list command', () => {
|
||||
merged: { ...defaultMergedSettings, mcpServers: {} },
|
||||
});
|
||||
|
||||
await listMcpServers();
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith('No MCP servers configured.');
|
||||
});
|
||||
@@ -162,10 +172,16 @@ describe('mcp list command', () => {
|
||||
isTrusted: true,
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
mockClient.connect.mockResolvedValue(undefined);
|
||||
mockClient.ping.mockResolvedValue(undefined);
|
||||
|
||||
await listMcpServers();
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith('Configured MCP servers:\n');
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
@@ -206,9 +222,15 @@ describe('mcp list command', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
mockClient.connect.mockRejectedValue(new Error('Connection failed'));
|
||||
|
||||
await listMcpServers();
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -236,10 +258,16 @@ describe('mcp list command', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
mockClient.connect.mockResolvedValue(undefined);
|
||||
mockClient.ping.mockResolvedValue(undefined);
|
||||
|
||||
await listMcpServers();
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -276,13 +304,22 @@ describe('mcp list command', () => {
|
||||
merged: settingsWithAllowlist,
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
mockClient.connect.mockResolvedValue(undefined);
|
||||
mockClient.ping.mockResolvedValue(undefined);
|
||||
|
||||
await listMcpServers({
|
||||
merged: settingsWithAllowlist,
|
||||
isTrusted: true,
|
||||
} as unknown as LoadedSettings);
|
||||
await listMcpServers(
|
||||
{} as unknown as CliArgs,
|
||||
{
|
||||
merged: settingsWithAllowlist,
|
||||
isTrusted: true,
|
||||
} as unknown as LoadedSettings,
|
||||
);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('allowed-server'),
|
||||
@@ -310,10 +347,16 @@ describe('mcp list command', () => {
|
||||
isTrusted: false,
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
// createTransport will throw in core if not trusted
|
||||
mockedCreateTransport.mockRejectedValue(new Error('Folder not trusted'));
|
||||
|
||||
await listMcpServers();
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -337,7 +380,13 @@ describe('mcp list command', () => {
|
||||
isTrusted: true,
|
||||
});
|
||||
|
||||
await listMcpServers();
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
@@ -359,12 +408,18 @@ describe('mcp list command', () => {
|
||||
isTrusted: true,
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionManager),
|
||||
};
|
||||
(loadCliConfig as Mock).mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.spyOn(
|
||||
McpServerEnablementManager.prototype,
|
||||
'isFileEnabled',
|
||||
).mockResolvedValue(false);
|
||||
|
||||
await listMcpServers();
|
||||
await listMcpServers({} as unknown as CliArgs);
|
||||
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
|
||||
@@ -17,36 +17,32 @@ import {
|
||||
debugLogger,
|
||||
applyAdminAllowlist,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
type GeminiCLIExtension,
|
||||
type MCPServerConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import type { ExtensionManager } from '../../config/extension-manager.js';
|
||||
import {
|
||||
canLoadServer,
|
||||
McpServerEnablementManager,
|
||||
} from '../../config/mcp/index.js';
|
||||
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export async function getMcpServersFromConfig(
|
||||
settings?: MergedSettings,
|
||||
settings: MergedSettings,
|
||||
extensionManager: ExtensionManager,
|
||||
): Promise<{
|
||||
mcpServers: Record<string, MCPServerConfig>;
|
||||
blockedServerNames: string[];
|
||||
}> {
|
||||
if (!settings) {
|
||||
settings = loadSettings().merged;
|
||||
let extensions: GeminiCLIExtension[];
|
||||
try {
|
||||
extensions = extensionManager.getExtensions();
|
||||
} catch {
|
||||
extensions = await extensionManager.loadExtensions();
|
||||
}
|
||||
|
||||
const extensionManager = new ExtensionManager({
|
||||
settings,
|
||||
workspaceDir: process.cwd(),
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
requestSetting: promptForSetting,
|
||||
});
|
||||
const extensions = await extensionManager.loadExtensions();
|
||||
const mcpServers = { ...settings.mcpServers };
|
||||
for (const extension of extensions) {
|
||||
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||
@@ -170,13 +166,23 @@ async function getServerStatus(
|
||||
}
|
||||
|
||||
export async function listMcpServers(
|
||||
argv: CliArgs,
|
||||
loadedSettingsArg?: LoadedSettings,
|
||||
): Promise<void> {
|
||||
const loadedSettings = loadedSettingsArg ?? loadSettings();
|
||||
const activeSettings = loadedSettings.merged;
|
||||
|
||||
const { mcpServers, blockedServerNames } =
|
||||
await getMcpServersFromConfig(activeSettings);
|
||||
const config = await loadCliConfig(activeSettings, 'mcp-list-session', argv, {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
await config.initialize();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const extensionManager = config.getExtensionLoader() as ExtensionManager;
|
||||
|
||||
const { mcpServers, blockedServerNames } = await getMcpServersFromConfig(
|
||||
activeSettings,
|
||||
extensionManager,
|
||||
);
|
||||
const serverNames = Object.keys(mcpServers);
|
||||
|
||||
if (blockedServerNames.length > 0) {
|
||||
@@ -257,7 +263,8 @@ export const listCommand: CommandModule<object, ListArgs> = {
|
||||
command: 'list',
|
||||
describe: 'List all configured MCP servers',
|
||||
handler: async (argv) => {
|
||||
await listMcpServers(argv.loadedSettings);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
await listMcpServers(argv as unknown as CliArgs, argv.loadedSettings);
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { coreEvents } from '@google/gemini-cli-core';
|
||||
import { handleList, listCommand } from './list.js';
|
||||
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
|
||||
import { loadCliConfig } from '../../config/config.js';
|
||||
import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('skills list command', () => {
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await handleList({});
|
||||
await handleList({} as unknown as CliArgs, {});
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -86,7 +86,7 @@ describe('skills list command', () => {
|
||||
};
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
await handleList({});
|
||||
await handleList({} as unknown as CliArgs, {});
|
||||
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
@@ -135,7 +135,7 @@ describe('skills list command', () => {
|
||||
mockLoadCliConfig.mockResolvedValue(mockConfig as unknown as Config);
|
||||
|
||||
// Default
|
||||
await handleList({ all: false });
|
||||
await handleList({} as unknown as CliArgs, { all: false });
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
expect.stringContaining('regular'),
|
||||
@@ -148,7 +148,7 @@ describe('skills list command', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// With all: true
|
||||
await handleList({ all: true });
|
||||
await handleList({} as unknown as CliArgs, { all: true });
|
||||
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
|
||||
'log',
|
||||
expect.stringContaining('regular'),
|
||||
@@ -166,7 +166,9 @@ describe('skills list command', () => {
|
||||
it('should throw an error when listing fails', async () => {
|
||||
mockLoadCliConfig.mockRejectedValue(new Error('List failed'));
|
||||
|
||||
await expect(handleList({})).rejects.toThrow('List failed');
|
||||
await expect(handleList({} as unknown as CliArgs, {})).rejects.toThrow(
|
||||
'List failed',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,17 +11,14 @@ import { loadCliConfig, type CliArgs } from '../../config/config.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export async function handleList(args: { all?: boolean }) {
|
||||
export async function handleList(argv: CliArgs, args: { all?: boolean }) {
|
||||
const workspaceDir = process.cwd();
|
||||
const settings = loadSettings(workspaceDir);
|
||||
|
||||
const config = await loadCliConfig(
|
||||
settings.merged,
|
||||
'skills-list-session',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
{
|
||||
debug: false,
|
||||
} as Partial<CliArgs> as CliArgs,
|
||||
argv,
|
||||
{ cwd: workspaceDir },
|
||||
);
|
||||
|
||||
@@ -73,8 +70,9 @@ export const listCommand: CommandModule = {
|
||||
default: false,
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
const args = { all: Boolean(argv['all']) };
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
await handleList({ all: argv['all'] as boolean });
|
||||
await handleList(argv as unknown as CliArgs, args);
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -109,6 +109,13 @@ export async function parseArguments(
|
||||
.usage(
|
||||
'Usage: gemini [options] [command]\n\nGemini CLI - Defaults to interactive mode. Use -p/--prompt for non-interactive (headless) mode.',
|
||||
)
|
||||
.option('profile', {
|
||||
alias: ['profiles', 'P'],
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
global: true,
|
||||
description: 'The name of the profile to use for this session.',
|
||||
})
|
||||
.option('debug', {
|
||||
alias: 'd',
|
||||
type: 'boolean',
|
||||
@@ -146,13 +153,6 @@ export async function parseArguments(
|
||||
type: 'boolean',
|
||||
description: 'Run in sandbox?',
|
||||
})
|
||||
.option('profile', {
|
||||
alias: ['profiles', 'P'],
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
description: 'The name of the profile to use for this session.',
|
||||
})
|
||||
|
||||
.option('yolo', {
|
||||
alias: 'y',
|
||||
type: 'boolean',
|
||||
|
||||
Reference in New Issue
Block a user