mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
Rationalize different Extension typings (#10435)
This commit is contained in:
@@ -20,16 +20,16 @@ import {
|
|||||||
GEMINI_CONFIG_DIR,
|
GEMINI_CONFIG_DIR,
|
||||||
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
|
type GeminiCLIExtension,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
import type { Settings } from './settings.js';
|
import type { Settings } from './settings.js';
|
||||||
import type { Extension } from './extension.js';
|
|
||||||
import { type AgentSettings, CoderAgentEvent } from '../types.js';
|
import { type AgentSettings, CoderAgentEvent } from '../types.js';
|
||||||
|
|
||||||
export async function loadConfig(
|
export async function loadConfig(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
extensions: Extension[],
|
extensions: GeminiCLIExtension[],
|
||||||
taskId: string,
|
taskId: string,
|
||||||
): Promise<Config> {
|
): Promise<Config> {
|
||||||
const mcpServers = mergeMcpServers(settings, extensions);
|
const mcpServers = mergeMcpServers(settings, extensions);
|
||||||
@@ -118,20 +118,21 @@ export async function loadConfig(
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeMcpServers(settings: Settings, extensions: Extension[]) {
|
export function mergeMcpServers(
|
||||||
|
settings: Settings,
|
||||||
|
extensions: GeminiCLIExtension[],
|
||||||
|
) {
|
||||||
const mcpServers = { ...(settings.mcpServers || {}) };
|
const mcpServers = { ...(settings.mcpServers || {}) };
|
||||||
for (const extension of extensions) {
|
for (const extension of extensions) {
|
||||||
Object.entries(extension.config.mcpServers || {}).forEach(
|
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||||
([key, server]) => {
|
if (mcpServers[key]) {
|
||||||
if (mcpServers[key]) {
|
console.warn(
|
||||||
console.warn(
|
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
|
||||||
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
|
);
|
||||||
);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
mcpServers[key] = server;
|
||||||
mcpServers[key] = server;
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return mcpServers;
|
return mcpServers;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
|
|
||||||
// Copied exactly from packages/cli/src/config/extension.ts, last PR #1026
|
// Copied exactly from packages/cli/src/config/extension.ts, last PR #1026
|
||||||
|
|
||||||
import type { MCPServerConfig } from '@google/gemini-cli-core';
|
import type {
|
||||||
|
MCPServerConfig,
|
||||||
|
ExtensionInstallMetadata,
|
||||||
|
GeminiCLIExtension,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
@@ -14,47 +18,51 @@ import { logger } from '../utils/logger.js';
|
|||||||
|
|
||||||
export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
|
export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions');
|
||||||
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
|
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
|
||||||
|
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
|
||||||
|
|
||||||
export interface Extension {
|
/**
|
||||||
config: ExtensionConfig;
|
* Extension definition as written to disk in gemini-extension.json files.
|
||||||
contextFiles: string[];
|
* This should *not* be referenced outside of the logic for reading files.
|
||||||
}
|
* If information is required for manipulating extensions (load, unload, update)
|
||||||
|
* outside of the loading process that data needs to be stored on the
|
||||||
export interface ExtensionConfig {
|
* GeminiCLIExtension class defined in Core.
|
||||||
|
*/
|
||||||
|
interface ExtensionConfig {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
mcpServers?: Record<string, MCPServerConfig>;
|
mcpServers?: Record<string, MCPServerConfig>;
|
||||||
contextFileName?: string | string[];
|
contextFileName?: string | string[];
|
||||||
|
excludeTools?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadExtensions(workspaceDir: string): Extension[] {
|
export function loadExtensions(workspaceDir: string): GeminiCLIExtension[] {
|
||||||
const allExtensions = [
|
const allExtensions = [
|
||||||
...loadExtensionsFromDir(workspaceDir),
|
...loadExtensionsFromDir(workspaceDir),
|
||||||
...loadExtensionsFromDir(os.homedir()),
|
...loadExtensionsFromDir(os.homedir()),
|
||||||
];
|
];
|
||||||
|
|
||||||
const uniqueExtensions: Extension[] = [];
|
const uniqueExtensions: GeminiCLIExtension[] = [];
|
||||||
const seenNames = new Set<string>();
|
const seenNames = new Set<string>();
|
||||||
for (const extension of allExtensions) {
|
for (const extension of allExtensions) {
|
||||||
if (!seenNames.has(extension.config.name)) {
|
if (!seenNames.has(extension.name)) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Loading extension: ${extension.config.name} (version: ${extension.config.version})`,
|
`Loading extension: ${extension.name} (version: ${extension.version})`,
|
||||||
);
|
);
|
||||||
uniqueExtensions.push(extension);
|
uniqueExtensions.push(extension);
|
||||||
seenNames.add(extension.config.name);
|
seenNames.add(extension.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return uniqueExtensions;
|
return uniqueExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadExtensionsFromDir(dir: string): Extension[] {
|
function loadExtensionsFromDir(dir: string): GeminiCLIExtension[] {
|
||||||
const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME);
|
const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME);
|
||||||
if (!fs.existsSync(extensionsDir)) {
|
if (!fs.existsSync(extensionsDir)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
for (const subdir of fs.readdirSync(extensionsDir)) {
|
for (const subdir of fs.readdirSync(extensionsDir)) {
|
||||||
const extensionDir = path.join(extensionsDir, subdir);
|
const extensionDir = path.join(extensionsDir, subdir);
|
||||||
|
|
||||||
@@ -66,7 +74,7 @@ function loadExtensionsFromDir(dir: string): Extension[] {
|
|||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadExtension(extensionDir: string): Extension | null {
|
function loadExtension(extensionDir: string): GeminiCLIExtension | null {
|
||||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Warning: unexpected file ${extensionDir} in extensions directory.`,
|
`Warning: unexpected file ${extensionDir} in extensions directory.`,
|
||||||
@@ -92,14 +100,22 @@ function loadExtension(extensionDir: string): Extension | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const installMetadata = loadInstallMetadata(extensionDir);
|
||||||
|
|
||||||
const contextFiles = getContextFileNames(config)
|
const contextFiles = getContextFileNames(config)
|
||||||
.map((contextFileName) => path.join(extensionDir, contextFileName))
|
.map((contextFileName) => path.join(extensionDir, contextFileName))
|
||||||
.filter((contextFilePath) => fs.existsSync(contextFilePath));
|
.filter((contextFilePath) => fs.existsSync(contextFilePath));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
name: config.name,
|
||||||
|
version: config.version,
|
||||||
|
path: extensionDir,
|
||||||
contextFiles,
|
contextFiles,
|
||||||
};
|
installMetadata,
|
||||||
|
mcpServers: config.mcpServers,
|
||||||
|
excludeTools: config.excludeTools,
|
||||||
|
isActive: true, // Barring any other signals extensions should be considered Active.
|
||||||
|
} as GeminiCLIExtension;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Warning: error parsing extension config in ${configFilePath}: ${e}`,
|
`Warning: error parsing extension config in ${configFilePath}: ${e}`,
|
||||||
@@ -116,3 +132,19 @@ function getContextFileNames(config: ExtensionConfig): string[] {
|
|||||||
}
|
}
|
||||||
return config.contextFileName;
|
return config.contextFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadInstallMetadata(
|
||||||
|
extensionDir: string,
|
||||||
|
): ExtensionInstallMetadata | undefined {
|
||||||
|
const metadataFilePath = path.join(extensionDir, INSTALL_METADATA_FILENAME);
|
||||||
|
try {
|
||||||
|
const configContent = fs.readFileSync(metadataFilePath, 'utf-8');
|
||||||
|
const metadata = JSON.parse(configContent) as ExtensionInstallMetadata;
|
||||||
|
return metadata;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to load or parse extension install metadata at ${metadataFilePath}: ${e}`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,10 +153,8 @@ describe('mcp list command', () => {
|
|||||||
|
|
||||||
mockedLoadExtensions.mockReturnValue([
|
mockedLoadExtensions.mockReturnValue([
|
||||||
{
|
{
|
||||||
config: {
|
name: 'test-extension',
|
||||||
name: 'test-extension',
|
mcpServers: { 'extension-server': { command: '/ext/server' } },
|
||||||
mcpServers: { 'extension-server': { command: '/ext/server' } },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -27,17 +27,15 @@ async function getMcpServersFromConfig(): Promise<
|
|||||||
);
|
);
|
||||||
const mcpServers = { ...(settings.merged.mcpServers || {}) };
|
const mcpServers = { ...(settings.merged.mcpServers || {}) };
|
||||||
for (const extension of extensions) {
|
for (const extension of extensions) {
|
||||||
Object.entries(extension.config.mcpServers || {}).forEach(
|
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||||
([key, server]) => {
|
if (mcpServers[key]) {
|
||||||
if (mcpServers[key]) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
mcpServers[key] = {
|
||||||
mcpServers[key] = {
|
...server,
|
||||||
...server,
|
extensionName: extension.name,
|
||||||
extensionName: extension.config.name,
|
};
|
||||||
};
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return mcpServers;
|
return mcpServers;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import {
|
|||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_GEMINI_MODEL_AUTO,
|
DEFAULT_GEMINI_MODEL_AUTO,
|
||||||
OutputFormat,
|
OutputFormat,
|
||||||
|
type GeminiCLIExtension,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
||||||
import type { Settings } from './settings.js';
|
import type { Settings } from './settings.js';
|
||||||
import { ExtensionStorage, type Extension } from './extension.js';
|
import { ExtensionStorage } from './extension.js';
|
||||||
import * as ServerConfig from '@google/gemini-cli-core';
|
import * as ServerConfig from '@google/gemini-cli-core';
|
||||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||||
@@ -1098,33 +1099,30 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
|||||||
it('should pass extension context file paths to loadServerHierarchicalMemory', async () => {
|
it('should pass extension context file paths to loadServerHierarchicalMemory', async () => {
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [
|
const extensions: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext1',
|
path: '/path/to/ext1',
|
||||||
config: {
|
name: 'ext1',
|
||||||
name: 'ext1',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
|
||||||
},
|
|
||||||
contextFiles: ['/path/to/ext1/GEMINI.md'],
|
contextFiles: ['/path/to/ext1/GEMINI.md'],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/path/to/ext2',
|
path: '/path/to/ext2',
|
||||||
config: {
|
name: 'ext2',
|
||||||
name: 'ext2',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
|
||||||
},
|
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/path/to/ext3',
|
path: '/path/to/ext3',
|
||||||
config: {
|
name: 'ext3',
|
||||||
name: 'ext3',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
|
||||||
},
|
|
||||||
contextFiles: [
|
contextFiles: [
|
||||||
'/path/to/ext3/context1.md',
|
'/path/to/ext3/context1.md',
|
||||||
'/path/to/ext3/context2.md',
|
'/path/to/ext3/context2.md',
|
||||||
],
|
],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
@@ -1195,19 +1193,18 @@ describe('mergeMcpServers', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const extensions: Extension[] = [
|
const extensions: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext1',
|
path: '/path/to/ext1',
|
||||||
config: {
|
name: 'ext1',
|
||||||
name: 'ext1',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
mcpServers: {
|
||||||
mcpServers: {
|
'ext1-server': {
|
||||||
'ext1-server': {
|
url: 'http://localhost:8081',
|
||||||
url: 'http://localhost:8081',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const originalSettings = JSON.parse(JSON.stringify(settings));
|
const originalSettings = JSON.parse(JSON.stringify(settings));
|
||||||
@@ -1241,24 +1238,22 @@ describe('mergeExcludeTools', () => {
|
|||||||
|
|
||||||
it('should merge excludeTools from settings and extensions', async () => {
|
it('should merge excludeTools from settings and extensions', async () => {
|
||||||
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
|
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
|
||||||
const extensions: Extension[] = [
|
const extensions: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext1',
|
path: '/path/to/ext1',
|
||||||
config: {
|
name: 'ext1',
|
||||||
name: 'ext1',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
excludeTools: ['tool3', 'tool4'],
|
||||||
excludeTools: ['tool3', 'tool4'],
|
|
||||||
},
|
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/path/to/ext2',
|
path: '/path/to/ext2',
|
||||||
config: {
|
name: 'ext2',
|
||||||
name: 'ext2',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
excludeTools: ['tool5'],
|
||||||
excludeTools: ['tool5'],
|
|
||||||
},
|
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
@@ -1281,15 +1276,14 @@ describe('mergeExcludeTools', () => {
|
|||||||
|
|
||||||
it('should handle overlapping excludeTools between settings and extensions', async () => {
|
it('should handle overlapping excludeTools between settings and extensions', async () => {
|
||||||
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
|
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
|
||||||
const extensions: Extension[] = [
|
const extensions: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext1',
|
path: '/path/to/ext1',
|
||||||
config: {
|
name: 'ext1',
|
||||||
name: 'ext1',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
excludeTools: ['tool2', 'tool3'],
|
||||||
excludeTools: ['tool2', 'tool3'],
|
|
||||||
},
|
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
@@ -1312,24 +1306,22 @@ describe('mergeExcludeTools', () => {
|
|||||||
|
|
||||||
it('should handle overlapping excludeTools between extensions', async () => {
|
it('should handle overlapping excludeTools between extensions', async () => {
|
||||||
const settings: Settings = { tools: { exclude: ['tool1'] } };
|
const settings: Settings = { tools: { exclude: ['tool1'] } };
|
||||||
const extensions: Extension[] = [
|
const extensions: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext1',
|
path: '/path/to/ext1',
|
||||||
config: {
|
name: 'ext1',
|
||||||
name: 'ext1',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
excludeTools: ['tool2', 'tool3'],
|
||||||
excludeTools: ['tool2', 'tool3'],
|
|
||||||
},
|
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/path/to/ext2',
|
path: '/path/to/ext2',
|
||||||
config: {
|
name: 'ext2',
|
||||||
name: 'ext2',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
excludeTools: ['tool3', 'tool4'],
|
||||||
excludeTools: ['tool3', 'tool4'],
|
|
||||||
},
|
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
@@ -1353,7 +1345,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
it('should return an empty array when no excludeTools are specified and it is interactive', async () => {
|
it('should return an empty array when no excludeTools are specified and it is interactive', async () => {
|
||||||
process.stdin.isTTY = true;
|
process.stdin.isTTY = true;
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
@@ -1372,7 +1364,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
it('should return default excludes when no excludeTools are specified and it is not interactive', async () => {
|
it('should return default excludes when no excludeTools are specified and it is not interactive', async () => {
|
||||||
process.stdin.isTTY = false;
|
process.stdin.isTTY = false;
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
@@ -1392,7 +1384,7 @@ describe('mergeExcludeTools', () => {
|
|||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
|
const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } };
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
extensions,
|
extensions,
|
||||||
@@ -1411,15 +1403,14 @@ describe('mergeExcludeTools', () => {
|
|||||||
|
|
||||||
it('should handle extensions with excludeTools but no settings', async () => {
|
it('should handle extensions with excludeTools but no settings', async () => {
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [
|
const extensions: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext',
|
path: '/path/to/ext',
|
||||||
config: {
|
name: 'ext1',
|
||||||
name: 'ext1',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
excludeTools: ['tool1', 'tool2'],
|
||||||
excludeTools: ['tool1', 'tool2'],
|
|
||||||
},
|
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
process.argv = ['node', 'script.js'];
|
process.argv = ['node', 'script.js'];
|
||||||
@@ -1442,15 +1433,14 @@ describe('mergeExcludeTools', () => {
|
|||||||
|
|
||||||
it('should not modify the original settings object', async () => {
|
it('should not modify the original settings object', async () => {
|
||||||
const settings: Settings = { tools: { exclude: ['tool1'] } };
|
const settings: Settings = { tools: { exclude: ['tool1'] } };
|
||||||
const extensions: Extension[] = [
|
const extensions: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext',
|
path: '/path/to/ext',
|
||||||
config: {
|
name: 'ext1',
|
||||||
name: 'ext1',
|
version: '1.0.0',
|
||||||
version: '1.0.0',
|
excludeTools: ['tool2'],
|
||||||
excludeTools: ['tool2'],
|
|
||||||
},
|
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const originalSettings = JSON.parse(JSON.stringify(settings));
|
const originalSettings = JSON.parse(JSON.stringify(settings));
|
||||||
@@ -1486,7 +1476,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
process.argv = ['node', 'script.js', '-p', 'test'];
|
process.argv = ['node', 'script.js', '-p', 'test'];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
|
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1516,7 +1506,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
];
|
];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
|
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1546,7 +1536,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
];
|
];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
|
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1576,7 +1566,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
];
|
];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
|
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1599,7 +1589,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
|
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
|
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1633,7 +1623,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
process.argv = testCase.args;
|
process.argv = testCase.args;
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
|
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1664,7 +1654,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
];
|
];
|
||||||
const argv = await parseArguments({} as Settings);
|
const argv = await parseArguments({} as Settings);
|
||||||
const settings: Settings = { tools: { exclude: ['custom_tool'] } };
|
const settings: Settings = { tools: { exclude: ['custom_tool'] } };
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
|
|
||||||
const config = await loadCliConfig(
|
const config = await loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1694,7 +1684,7 @@ describe('Approval mode tool exclusion logic', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const settings: Settings = {};
|
const settings: Settings = {};
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
await expect(
|
await expect(
|
||||||
loadCliConfig(
|
loadCliConfig(
|
||||||
settings,
|
settings,
|
||||||
@@ -1976,16 +1966,20 @@ describe('loadCliConfig with allowed-mcp-server-names', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('loadCliConfig extensions', () => {
|
describe('loadCliConfig extensions', () => {
|
||||||
const mockExtensions: Extension[] = [
|
const mockExtensions: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext1',
|
path: '/path/to/ext1',
|
||||||
config: { name: 'ext1', version: '1.0.0' },
|
name: 'ext1',
|
||||||
|
version: '1.0.0',
|
||||||
contextFiles: ['/path/to/ext1.md'],
|
contextFiles: ['/path/to/ext1.md'],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/path/to/ext2',
|
path: '/path/to/ext2',
|
||||||
config: { name: 'ext2', version: '1.0.0' },
|
name: 'ext2',
|
||||||
|
version: '1.0.0',
|
||||||
contextFiles: ['/path/to/ext2.md'],
|
contextFiles: ['/path/to/ext2.md'],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
FileFilteringOptions,
|
FileFilteringOptions,
|
||||||
MCPServerConfig,
|
MCPServerConfig,
|
||||||
OutputFormat,
|
OutputFormat,
|
||||||
|
GeminiCLIExtension,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { extensionsCommand } from '../commands/extensions.js';
|
import { extensionsCommand } from '../commands/extensions.js';
|
||||||
import {
|
import {
|
||||||
@@ -37,7 +38,6 @@ import {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { Settings } from './settings.js';
|
import type { Settings } from './settings.js';
|
||||||
|
|
||||||
import type { Extension } from './extension.js';
|
|
||||||
import { annotateActiveExtensions } from './extension.js';
|
import { annotateActiveExtensions } from './extension.js';
|
||||||
import { getCliVersion } from '../utils/version.js';
|
import { getCliVersion } from '../utils/version.js';
|
||||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||||
@@ -472,7 +472,7 @@ export function isDebugMode(argv: CliArgs): boolean {
|
|||||||
|
|
||||||
export async function loadCliConfig(
|
export async function loadCliConfig(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
extensions: Extension[],
|
extensions: GeminiCLIExtension[],
|
||||||
extensionEnablementManager: ExtensionEnablementManager,
|
extensionEnablementManager: ExtensionEnablementManager,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
argv: CliArgs,
|
argv: CliArgs,
|
||||||
@@ -787,30 +787,28 @@ function allowedMcpServers(
|
|||||||
return mcpServers;
|
return mcpServers;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeMcpServers(settings: Settings, extensions: Extension[]) {
|
function mergeMcpServers(settings: Settings, extensions: GeminiCLIExtension[]) {
|
||||||
const mcpServers = { ...(settings.mcpServers || {}) };
|
const mcpServers = { ...(settings.mcpServers || {}) };
|
||||||
for (const extension of extensions) {
|
for (const extension of extensions) {
|
||||||
Object.entries(extension.config.mcpServers || {}).forEach(
|
Object.entries(extension.mcpServers || {}).forEach(([key, server]) => {
|
||||||
([key, server]) => {
|
if (mcpServers[key]) {
|
||||||
if (mcpServers[key]) {
|
logger.warn(
|
||||||
logger.warn(
|
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
|
||||||
`Skipping extension MCP config for server with key "${key}" as it already exists.`,
|
);
|
||||||
);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
mcpServers[key] = {
|
||||||
mcpServers[key] = {
|
...server,
|
||||||
...server,
|
extensionName: extension.name,
|
||||||
extensionName: extension.config.name,
|
};
|
||||||
};
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return mcpServers;
|
return mcpServers;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeExcludeTools(
|
function mergeExcludeTools(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
extensions: Extension[],
|
extensions: GeminiCLIExtension[],
|
||||||
extraExcludes?: string[] | undefined,
|
extraExcludes?: string[] | undefined,
|
||||||
): string[] {
|
): string[] {
|
||||||
const allExcludeTools = new Set([
|
const allExcludeTools = new Set([
|
||||||
@@ -818,7 +816,7 @@ function mergeExcludeTools(
|
|||||||
...(extraExcludes || []),
|
...(extraExcludes || []),
|
||||||
]);
|
]);
|
||||||
for (const extension of extensions) {
|
for (const extension of extensions) {
|
||||||
for (const tool of extension.config.excludeTools || []) {
|
for (const tool of extension.excludeTools || []) {
|
||||||
allExcludeTools.add(tool);
|
allExcludeTools.add(tool);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
performWorkspaceExtensionMigration,
|
performWorkspaceExtensionMigration,
|
||||||
requestConsentNonInteractive,
|
requestConsentNonInteractive,
|
||||||
uninstallExtension,
|
uninstallExtension,
|
||||||
type Extension,
|
|
||||||
} from './extension.js';
|
} from './extension.js';
|
||||||
import {
|
import {
|
||||||
GEMINI_DIR,
|
GEMINI_DIR,
|
||||||
@@ -158,7 +157,7 @@ describe('extension tests', () => {
|
|||||||
);
|
);
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
expect(extensions[0].path).toBe(extensionDir);
|
expect(extensions[0].path).toBe(extensionDir);
|
||||||
expect(extensions[0].config.name).toBe('test-extension');
|
expect(extensions[0].name).toBe('test-extension');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load context file path when GEMINI.md is present', () => {
|
it('should load context file path when GEMINI.md is present', () => {
|
||||||
@@ -179,8 +178,8 @@ describe('extension tests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(extensions).toHaveLength(2);
|
expect(extensions).toHaveLength(2);
|
||||||
const ext1 = extensions.find((e) => e.config.name === 'ext1');
|
const ext1 = extensions.find((e) => e.name === 'ext1');
|
||||||
const ext2 = extensions.find((e) => e.config.name === 'ext2');
|
const ext2 = extensions.find((e) => e.name === 'ext2');
|
||||||
expect(ext1?.contextFiles).toEqual([
|
expect(ext1?.contextFiles).toEqual([
|
||||||
path.join(userExtensionsDir, 'ext1', 'GEMINI.md'),
|
path.join(userExtensionsDir, 'ext1', 'GEMINI.md'),
|
||||||
]);
|
]);
|
||||||
@@ -201,7 +200,7 @@ describe('extension tests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
const ext1 = extensions.find((e) => e.config.name === 'ext1');
|
const ext1 = extensions.find((e) => e.name === 'ext1');
|
||||||
expect(ext1?.contextFiles).toEqual([
|
expect(ext1?.contextFiles).toEqual([
|
||||||
path.join(userExtensionsDir, 'ext1', 'my-context-file.md'),
|
path.join(userExtensionsDir, 'ext1', 'my-context-file.md'),
|
||||||
]);
|
]);
|
||||||
@@ -254,13 +253,12 @@ describe('extension tests', () => {
|
|||||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||||
);
|
);
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
const loadedConfig = extensions[0].config;
|
|
||||||
const expectedCwd = path.join(
|
const expectedCwd = path.join(
|
||||||
userExtensionsDir,
|
userExtensionsDir,
|
||||||
'test-extension',
|
'test-extension',
|
||||||
'server',
|
'server',
|
||||||
);
|
);
|
||||||
expect(loadedConfig.mcpServers?.['test-server'].cwd).toBe(expectedCwd);
|
expect(extensions[0].mcpServers?.['test-server'].cwd).toBe(expectedCwd);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load a linked extension correctly', async () => {
|
it('should load a linked extension correctly', async () => {
|
||||||
@@ -287,7 +285,7 @@ describe('extension tests', () => {
|
|||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
|
|
||||||
const linkedExt = extensions[0];
|
const linkedExt = extensions[0];
|
||||||
expect(linkedExt.config.name).toBe('my-linked-extension');
|
expect(linkedExt.name).toBe('my-linked-extension');
|
||||||
|
|
||||||
expect(linkedExt.path).toBe(sourceExtDir);
|
expect(linkedExt.path).toBe(sourceExtDir);
|
||||||
expect(linkedExt.installMetadata).toEqual({
|
expect(linkedExt.installMetadata).toEqual({
|
||||||
@@ -340,10 +338,10 @@ describe('extension tests', () => {
|
|||||||
|
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
const extension = extensions[0];
|
const extension = extensions[0];
|
||||||
expect(extension.config.name).toBe('test-extension');
|
expect(extension.name).toBe('test-extension');
|
||||||
expect(extension.config.mcpServers).toBeDefined();
|
expect(extension.mcpServers).toBeDefined();
|
||||||
|
|
||||||
const serverConfig = extension.config.mcpServers?.['test-server'];
|
const serverConfig = extension.mcpServers?.['test-server'];
|
||||||
expect(serverConfig).toBeDefined();
|
expect(serverConfig).toBeDefined();
|
||||||
expect(serverConfig?.env).toBeDefined();
|
expect(serverConfig?.env).toBeDefined();
|
||||||
expect(serverConfig?.env?.API_KEY).toBe('test-api-key-123');
|
expect(serverConfig?.env?.API_KEY).toBe('test-api-key-123');
|
||||||
@@ -393,7 +391,7 @@ describe('extension tests', () => {
|
|||||||
|
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
const extension = extensions[0];
|
const extension = extensions[0];
|
||||||
const serverConfig = extension.config.mcpServers!['test-server'];
|
const serverConfig = extension.mcpServers!['test-server'];
|
||||||
expect(serverConfig.env).toBeDefined();
|
expect(serverConfig.env).toBeDefined();
|
||||||
expect(serverConfig.env!.MISSING_VAR).toBe('$UNDEFINED_ENV_VAR');
|
expect(serverConfig.env!.MISSING_VAR).toBe('$UNDEFINED_ENV_VAR');
|
||||||
expect(serverConfig.env!.MISSING_VAR_BRACES).toBe('${ALSO_UNDEFINED}');
|
expect(serverConfig.env!.MISSING_VAR_BRACES).toBe('${ALSO_UNDEFINED}');
|
||||||
@@ -422,7 +420,7 @@ describe('extension tests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
expect(extensions[0].config.name).toBe('good-ext');
|
expect(extensions[0].name).toBe('good-ext');
|
||||||
expect(consoleSpy).toHaveBeenCalledOnce();
|
expect(consoleSpy).toHaveBeenCalledOnce();
|
||||||
expect(consoleSpy).toHaveBeenCalledWith(
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
@@ -456,7 +454,7 @@ describe('extension tests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
expect(extensions[0].config.name).toBe('good-ext');
|
expect(extensions[0].name).toBe('good-ext');
|
||||||
expect(consoleSpy).toHaveBeenCalledOnce();
|
expect(consoleSpy).toHaveBeenCalledOnce();
|
||||||
expect(consoleSpy).toHaveBeenCalledWith(
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
@@ -485,8 +483,7 @@ describe('extension tests', () => {
|
|||||||
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
|
||||||
);
|
);
|
||||||
expect(extensions).toHaveLength(1);
|
expect(extensions).toHaveLength(1);
|
||||||
const loadedConfig = extensions[0].config;
|
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
|
||||||
expect(loadedConfig.mcpServers?.['test-server'].trust).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error for invalid extension names', () => {
|
it('should throw an error for invalid extension names', () => {
|
||||||
@@ -513,21 +510,27 @@ describe('extension tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('annotateActiveExtensions', () => {
|
describe('annotateActiveExtensions', () => {
|
||||||
const extensions: Extension[] = [
|
const extensions: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext1',
|
path: '/path/to/ext1',
|
||||||
config: { name: 'ext1', version: '1.0.0' },
|
name: 'ext1',
|
||||||
|
version: '1.0.0',
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/path/to/ext2',
|
path: '/path/to/ext2',
|
||||||
config: { name: 'ext2', version: '1.0.0' },
|
name: 'ext2',
|
||||||
|
version: '1.0.0',
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/path/to/ext3',
|
path: '/path/to/ext3',
|
||||||
config: { name: 'ext3', version: '1.0.0' },
|
name: 'ext3',
|
||||||
|
version: '1.0.0',
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -622,13 +625,15 @@ describe('extension tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be true if autoUpdate is true in install metadata', () => {
|
it('should be true if autoUpdate is true in install metadata', () => {
|
||||||
const extensionsWithAutoUpdate: Extension[] = extensions.map((e) => ({
|
const extensionsWithAutoUpdate: GeminiCLIExtension[] = extensions.map(
|
||||||
...e,
|
(e) => ({
|
||||||
installMetadata: {
|
...e,
|
||||||
...e.installMetadata!,
|
installMetadata: {
|
||||||
autoUpdate: true,
|
...e.installMetadata!,
|
||||||
},
|
autoUpdate: true,
|
||||||
}));
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
const activeExtensions = annotateActiveExtensions(
|
const activeExtensions = annotateActiveExtensions(
|
||||||
extensionsWithAutoUpdate,
|
extensionsWithAutoUpdate,
|
||||||
tempHomeDir,
|
tempHomeDir,
|
||||||
@@ -642,31 +647,37 @@ describe('extension tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should respect the per-extension settings from install metadata', () => {
|
it('should respect the per-extension settings from install metadata', () => {
|
||||||
const extensionsWithAutoUpdate: Extension[] = [
|
const extensionsWithAutoUpdate: GeminiCLIExtension[] = [
|
||||||
{
|
{
|
||||||
path: '/path/to/ext1',
|
path: '/path/to/ext1',
|
||||||
config: { name: 'ext1', version: '1.0.0' },
|
name: 'ext1',
|
||||||
|
version: '1.0.0',
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
installMetadata: {
|
installMetadata: {
|
||||||
source: 'test',
|
source: 'test',
|
||||||
type: 'local',
|
type: 'local',
|
||||||
autoUpdate: true,
|
autoUpdate: true,
|
||||||
},
|
},
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/path/to/ext2',
|
path: '/path/to/ext2',
|
||||||
config: { name: 'ext2', version: '1.0.0' },
|
name: 'ext2',
|
||||||
|
version: '1.0.0',
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
installMetadata: {
|
installMetadata: {
|
||||||
source: 'test',
|
source: 'test',
|
||||||
type: 'local',
|
type: 'local',
|
||||||
autoUpdate: false,
|
autoUpdate: false,
|
||||||
},
|
},
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/path/to/ext3',
|
path: '/path/to/ext3',
|
||||||
config: { name: 'ext3', version: '1.0.0' },
|
name: 'ext3',
|
||||||
|
version: '1.0.0',
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const activeExtensions = annotateActiveExtensions(
|
const activeExtensions = annotateActiveExtensions(
|
||||||
@@ -1229,7 +1240,7 @@ This extension will run the following MCP servers:
|
|||||||
name: 'ext2',
|
name: 'ext2',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
});
|
});
|
||||||
const extensionsToMigrate: Extension[] = [
|
const extensionsToMigrate: GeminiCLIExtension[] = [
|
||||||
loadExtension({
|
loadExtension({
|
||||||
extensionDir: ext1Path,
|
extensionDir: ext1Path,
|
||||||
workspaceDir: tempWorkspaceDir,
|
workspaceDir: tempWorkspaceDir,
|
||||||
@@ -1273,15 +1284,17 @@ This extension will run the following MCP servers:
|
|||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
const extensions: Extension[] = [
|
const extensions: GeminiCLIExtension[] = [
|
||||||
loadExtension({
|
loadExtension({
|
||||||
extensionDir: ext1Path,
|
extensionDir: ext1Path,
|
||||||
workspaceDir: tempWorkspaceDir,
|
workspaceDir: tempWorkspaceDir,
|
||||||
})!,
|
})!,
|
||||||
{
|
{
|
||||||
path: '/ext/path/1',
|
path: '/ext/path/1',
|
||||||
config: { name: 'ext2', version: '1.0.0' },
|
name: 'ext2',
|
||||||
|
version: '1.0.0',
|
||||||
contextFiles: [],
|
contextFiles: [],
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
|
|||||||
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
|
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
|
||||||
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
|
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
|
||||||
|
|
||||||
export interface Extension {
|
/**
|
||||||
path: string;
|
* Extension definition as written to disk in gemini-extension.json files.
|
||||||
config: ExtensionConfig;
|
* This should *not* be referenced outside of the logic for reading files.
|
||||||
contextFiles: string[];
|
* If information is required for manipulating extensions (load, unload, update)
|
||||||
installMetadata?: ExtensionInstallMetadata | undefined;
|
* outside of the loading process that data needs to be stored on the
|
||||||
}
|
* GeminiCLIExtension class defined in Core.
|
||||||
|
*/
|
||||||
export interface ExtensionConfig {
|
interface ExtensionConfig {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
mcpServers?: Record<string, MCPServerConfig>;
|
mcpServers?: Record<string, MCPServerConfig>;
|
||||||
@@ -96,7 +96,9 @@ export class ExtensionStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkspaceExtensions(workspaceDir: string): Extension[] {
|
export function getWorkspaceExtensions(
|
||||||
|
workspaceDir: string,
|
||||||
|
): GeminiCLIExtension[] {
|
||||||
// If the workspace dir is the user extensions dir, there are no workspace extensions.
|
// If the workspace dir is the user extensions dir, there are no workspace extensions.
|
||||||
if (path.resolve(workspaceDir) === path.resolve(os.homedir())) {
|
if (path.resolve(workspaceDir) === path.resolve(os.homedir())) {
|
||||||
return [];
|
return [];
|
||||||
@@ -112,7 +114,7 @@ export async function copyExtension(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function performWorkspaceExtensionMigration(
|
export async function performWorkspaceExtensionMigration(
|
||||||
extensions: Extension[],
|
extensions: GeminiCLIExtension[],
|
||||||
requestConsent: (consent: string) => Promise<boolean>,
|
requestConsent: (consent: string) => Promise<boolean>,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const failedInstallNames: string[] = [];
|
const failedInstallNames: string[] = [];
|
||||||
@@ -125,7 +127,7 @@ export async function performWorkspaceExtensionMigration(
|
|||||||
};
|
};
|
||||||
await installExtension(installMetadata, requestConsent);
|
await installExtension(installMetadata, requestConsent);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
failedInstallNames.push(extension.config.name);
|
failedInstallNames.push(extension.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return failedInstallNames;
|
return failedInstallNames;
|
||||||
@@ -148,7 +150,7 @@ function getTelemetryConfig(cwd: string) {
|
|||||||
export function loadExtensions(
|
export function loadExtensions(
|
||||||
extensionEnablementManager: ExtensionEnablementManager,
|
extensionEnablementManager: ExtensionEnablementManager,
|
||||||
workspaceDir: string = process.cwd(),
|
workspaceDir: string = process.cwd(),
|
||||||
): Extension[] {
|
): GeminiCLIExtension[] {
|
||||||
const settings = loadSettings(workspaceDir).merged;
|
const settings = loadSettings(workspaceDir).merged;
|
||||||
const allExtensions = [...loadUserExtensions()];
|
const allExtensions = [...loadUserExtensions()];
|
||||||
|
|
||||||
@@ -160,41 +162,41 @@ export function loadExtensions(
|
|||||||
allExtensions.push(...getWorkspaceExtensions(workspaceDir));
|
allExtensions.push(...getWorkspaceExtensions(workspaceDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueExtensions = new Map<string, Extension>();
|
const uniqueExtensions = new Map<string, GeminiCLIExtension>();
|
||||||
|
|
||||||
for (const extension of allExtensions) {
|
for (const extension of allExtensions) {
|
||||||
if (
|
if (
|
||||||
!uniqueExtensions.has(extension.config.name) &&
|
!uniqueExtensions.has(extension.name) &&
|
||||||
extensionEnablementManager.isEnabled(extension.config.name, workspaceDir)
|
extensionEnablementManager.isEnabled(extension.name, workspaceDir)
|
||||||
) {
|
) {
|
||||||
uniqueExtensions.set(extension.config.name, extension);
|
uniqueExtensions.set(extension.name, extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(uniqueExtensions.values());
|
return Array.from(uniqueExtensions.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadUserExtensions(): Extension[] {
|
export function loadUserExtensions(): GeminiCLIExtension[] {
|
||||||
const userExtensions = loadExtensionsFromDir(os.homedir());
|
const userExtensions = loadExtensionsFromDir(os.homedir());
|
||||||
|
|
||||||
const uniqueExtensions = new Map<string, Extension>();
|
const uniqueExtensions = new Map<string, GeminiCLIExtension>();
|
||||||
for (const extension of userExtensions) {
|
for (const extension of userExtensions) {
|
||||||
if (!uniqueExtensions.has(extension.config.name)) {
|
if (!uniqueExtensions.has(extension.name)) {
|
||||||
uniqueExtensions.set(extension.config.name, extension);
|
uniqueExtensions.set(extension.name, extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(uniqueExtensions.values());
|
return Array.from(uniqueExtensions.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadExtensionsFromDir(dir: string): Extension[] {
|
export function loadExtensionsFromDir(dir: string): GeminiCLIExtension[] {
|
||||||
const storage = new Storage(dir);
|
const storage = new Storage(dir);
|
||||||
const extensionsDir = storage.getExtensionsDir();
|
const extensionsDir = storage.getExtensionsDir();
|
||||||
if (!fs.existsSync(extensionsDir)) {
|
if (!fs.existsSync(extensionsDir)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const extensions: Extension[] = [];
|
const extensions: GeminiCLIExtension[] = [];
|
||||||
for (const subdir of fs.readdirSync(extensionsDir)) {
|
for (const subdir of fs.readdirSync(extensionsDir)) {
|
||||||
const extensionDir = path.join(extensionsDir, subdir);
|
const extensionDir = path.join(extensionsDir, subdir);
|
||||||
|
|
||||||
@@ -206,7 +208,9 @@ export function loadExtensionsFromDir(dir: string): Extension[] {
|
|||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadExtension(context: LoadExtensionContext): Extension | null {
|
export function loadExtension(
|
||||||
|
context: LoadExtensionContext,
|
||||||
|
): GeminiCLIExtension | null {
|
||||||
const { extensionDir, workspaceDir } = context;
|
const { extensionDir, workspaceDir } = context;
|
||||||
if (!fs.statSync(extensionDir).isDirectory()) {
|
if (!fs.statSync(extensionDir).isDirectory()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -243,10 +247,14 @@ export function loadExtension(context: LoadExtensionContext): Extension | null {
|
|||||||
.filter((contextFilePath) => fs.existsSync(contextFilePath));
|
.filter((contextFilePath) => fs.existsSync(contextFilePath));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
name: config.name,
|
||||||
|
version: config.version,
|
||||||
path: effectiveExtensionPath,
|
path: effectiveExtensionPath,
|
||||||
config,
|
|
||||||
contextFiles,
|
contextFiles,
|
||||||
installMetadata,
|
installMetadata,
|
||||||
|
mcpServers: config.mcpServers,
|
||||||
|
excludeTools: config.excludeTools,
|
||||||
|
isActive: true, // Barring any other signals extensions should be considered Active.
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -261,7 +269,7 @@ export function loadExtension(context: LoadExtensionContext): Extension | null {
|
|||||||
export function loadExtensionByName(
|
export function loadExtensionByName(
|
||||||
name: string,
|
name: string,
|
||||||
workspaceDir: string = process.cwd(),
|
workspaceDir: string = process.cwd(),
|
||||||
): Extension | null {
|
): GeminiCLIExtension | null {
|
||||||
const userExtensionsDir = ExtensionStorage.getUserExtensionsDir();
|
const userExtensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||||
if (!fs.existsSync(userExtensionsDir)) {
|
if (!fs.existsSync(userExtensionsDir)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -273,10 +281,7 @@ export function loadExtensionByName(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const extension = loadExtension({ extensionDir, workspaceDir });
|
const extension = loadExtension({ extensionDir, workspaceDir });
|
||||||
if (
|
if (extension && extension.name.toLowerCase() === name.toLowerCase()) {
|
||||||
extension &&
|
|
||||||
extension.config.name.toLowerCase() === name.toLowerCase()
|
|
||||||
) {
|
|
||||||
return extension;
|
return extension;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,17 +325,14 @@ function getContextFileNames(config: ExtensionConfig): string[] {
|
|||||||
* @param workspaceDir The current workspace directory.
|
* @param workspaceDir The current workspace directory.
|
||||||
*/
|
*/
|
||||||
export function annotateActiveExtensions(
|
export function annotateActiveExtensions(
|
||||||
extensions: Extension[],
|
extensions: GeminiCLIExtension[],
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
manager: ExtensionEnablementManager,
|
manager: ExtensionEnablementManager,
|
||||||
): GeminiCLIExtension[] {
|
): GeminiCLIExtension[] {
|
||||||
manager.validateExtensionOverrides(extensions);
|
manager.validateExtensionOverrides(extensions);
|
||||||
return extensions.map((extension) => ({
|
return extensions.map((extension) => ({
|
||||||
name: extension.config.name,
|
...extension,
|
||||||
version: extension.config.version,
|
isActive: manager.isEnabled(extension.name, workspaceDir),
|
||||||
isActive: manager.isEnabled(extension.config.name, workspaceDir),
|
|
||||||
path: extension.path,
|
|
||||||
installMetadata: extension.installMetadata,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,7 +491,7 @@ export async function installExtension(
|
|||||||
const installedExtensions = loadUserExtensions();
|
const installedExtensions = loadUserExtensions();
|
||||||
if (
|
if (
|
||||||
installedExtensions.some(
|
installedExtensions.some(
|
||||||
(installed) => installed.config.name === newExtensionName,
|
(installed) => installed.name === newExtensionName,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -672,11 +674,10 @@ export async function uninstallExtension(
|
|||||||
const installedExtensions = loadUserExtensions();
|
const installedExtensions = loadUserExtensions();
|
||||||
const extensionName = installedExtensions.find(
|
const extensionName = installedExtensions.find(
|
||||||
(installed) =>
|
(installed) =>
|
||||||
installed.config.name.toLowerCase() ===
|
installed.name.toLowerCase() === extensionIdentifier.toLowerCase() ||
|
||||||
extensionIdentifier.toLowerCase() ||
|
|
||||||
installed.installMetadata?.source.toLowerCase() ===
|
installed.installMetadata?.source.toLowerCase() ===
|
||||||
extensionIdentifier.toLowerCase(),
|
extensionIdentifier.toLowerCase(),
|
||||||
)?.config.name;
|
)?.name;
|
||||||
if (!extensionName) {
|
if (!extensionName) {
|
||||||
throw new Error(`Extension not found.`);
|
throw new Error(`Extension not found.`);
|
||||||
}
|
}
|
||||||
@@ -698,20 +699,17 @@ export async function uninstallExtension(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toOutputString(
|
export function toOutputString(
|
||||||
extension: Extension,
|
extension: GeminiCLIExtension,
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
): string {
|
): string {
|
||||||
const manager = new ExtensionEnablementManager(
|
const manager = new ExtensionEnablementManager(
|
||||||
ExtensionStorage.getUserExtensionsDir(),
|
ExtensionStorage.getUserExtensionsDir(),
|
||||||
);
|
);
|
||||||
const userEnabled = manager.isEnabled(extension.config.name, os.homedir());
|
const userEnabled = manager.isEnabled(extension.name, os.homedir());
|
||||||
const workspaceEnabled = manager.isEnabled(
|
const workspaceEnabled = manager.isEnabled(extension.name, workspaceDir);
|
||||||
extension.config.name,
|
|
||||||
workspaceDir,
|
|
||||||
);
|
|
||||||
|
|
||||||
const status = workspaceEnabled ? chalk.green('✓') : chalk.red('✗');
|
const status = workspaceEnabled ? chalk.green('✓') : chalk.red('✗');
|
||||||
let output = `${status} ${extension.config.name} (${extension.config.version})`;
|
let output = `${status} ${extension.name} (${extension.version})`;
|
||||||
output += `\n Path: ${extension.path}`;
|
output += `\n Path: ${extension.path}`;
|
||||||
if (extension.installMetadata) {
|
if (extension.installMetadata) {
|
||||||
output += `\n Source: ${extension.installMetadata.source} (Type: ${extension.installMetadata.type})`;
|
output += `\n Source: ${extension.installMetadata.source} (Type: ${extension.installMetadata.type})`;
|
||||||
@@ -730,15 +728,15 @@ export function toOutputString(
|
|||||||
output += `\n ${contextFile}`;
|
output += `\n ${contextFile}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (extension.config.mcpServers) {
|
if (extension.mcpServers) {
|
||||||
output += `\n MCP servers:`;
|
output += `\n MCP servers:`;
|
||||||
Object.keys(extension.config.mcpServers).forEach((key) => {
|
Object.keys(extension.mcpServers).forEach((key) => {
|
||||||
output += `\n ${key}`;
|
output += `\n ${key}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (extension.config.excludeTools) {
|
if (extension.excludeTools) {
|
||||||
output += `\n Excluded tools:`;
|
output += `\n Excluded tools:`;
|
||||||
extension.config.excludeTools.forEach((tool) => {
|
extension.excludeTools.forEach((tool) => {
|
||||||
output += `\n ${tool}`;
|
output += `\n ${tool}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import fs from 'node:fs';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
import { ExtensionEnablementManager, Override } from './extensionEnablement.js';
|
import { ExtensionEnablementManager, Override } from './extensionEnablement.js';
|
||||||
import type { Extension } from '../extension.js';
|
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
// Helper to create a temporary directory for testing
|
// Helper to create a temporary directory for testing
|
||||||
function createTestDir() {
|
function createTestDir() {
|
||||||
@@ -286,9 +286,9 @@ describe('ExtensionEnablementManager', () => {
|
|||||||
'ext-two',
|
'ext-two',
|
||||||
]);
|
]);
|
||||||
const extensions = [
|
const extensions = [
|
||||||
{ config: { name: 'ext-one' } },
|
{ name: 'ext-one' },
|
||||||
{ config: { name: 'ext-two' } },
|
{ name: 'ext-two' },
|
||||||
] as Extension[];
|
] as GeminiCLIExtension[];
|
||||||
manager.validateExtensionOverrides(extensions);
|
manager.validateExtensionOverrides(extensions);
|
||||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -300,9 +300,9 @@ describe('ExtensionEnablementManager', () => {
|
|||||||
'ext-another-invalid',
|
'ext-another-invalid',
|
||||||
]);
|
]);
|
||||||
const extensions = [
|
const extensions = [
|
||||||
{ config: { name: 'ext-one' } },
|
{ name: 'ext-one' },
|
||||||
{ config: { name: 'ext-two' } },
|
{ name: 'ext-two' },
|
||||||
] as Extension[];
|
] as GeminiCLIExtension[];
|
||||||
manager.validateExtensionOverrides(extensions);
|
manager.validateExtensionOverrides(extensions);
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { type Extension } from '../extension.js';
|
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
export interface ExtensionEnablementConfig {
|
export interface ExtensionEnablementConfig {
|
||||||
overrides: string[];
|
overrides: string[];
|
||||||
@@ -119,13 +119,11 @@ export class ExtensionEnablementManager {
|
|||||||
enabledExtensionNames?.map((name) => name.toLowerCase()) ?? [];
|
enabledExtensionNames?.map((name) => name.toLowerCase()) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
validateExtensionOverrides(extensions: Extension[]) {
|
validateExtensionOverrides(extensions: GeminiCLIExtension[]) {
|
||||||
for (const name of this.enabledExtensionNamesOverride) {
|
for (const name of this.enabledExtensionNamesOverride) {
|
||||||
if (name === 'none') continue;
|
if (name === 'none') continue;
|
||||||
if (
|
if (
|
||||||
!extensions.some(
|
!extensions.some((ext) => ext.name.toLowerCase() === name.toLowerCase())
|
||||||
(ext) => ext.config.name.toLowerCase() === name.toLowerCase(),
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
console.error(`Extension not found: ${name}`);
|
console.error(`Extension not found: ${name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ describe('git extension helpers', () => {
|
|||||||
type: 'link',
|
type: 'link',
|
||||||
source: '',
|
source: '',
|
||||||
},
|
},
|
||||||
|
contextFiles: [],
|
||||||
};
|
};
|
||||||
const result = await checkForExtensionUpdate(extension);
|
const result = await checkForExtensionUpdate(extension);
|
||||||
expect(result).toBe(ExtensionUpdateState.NOT_UPDATABLE);
|
expect(result).toBe(ExtensionUpdateState.NOT_UPDATABLE);
|
||||||
@@ -152,6 +153,7 @@ describe('git extension helpers', () => {
|
|||||||
type: 'git',
|
type: 'git',
|
||||||
source: '',
|
source: '',
|
||||||
},
|
},
|
||||||
|
contextFiles: [],
|
||||||
};
|
};
|
||||||
mockGit.getRemotes.mockResolvedValue([]);
|
mockGit.getRemotes.mockResolvedValue([]);
|
||||||
const result = await checkForExtensionUpdate(extension);
|
const result = await checkForExtensionUpdate(extension);
|
||||||
@@ -168,6 +170,7 @@ describe('git extension helpers', () => {
|
|||||||
type: 'git',
|
type: 'git',
|
||||||
source: 'my/ext',
|
source: 'my/ext',
|
||||||
},
|
},
|
||||||
|
contextFiles: [],
|
||||||
};
|
};
|
||||||
mockGit.getRemotes.mockResolvedValue([
|
mockGit.getRemotes.mockResolvedValue([
|
||||||
{ name: 'origin', refs: { fetch: 'http://my-repo.com' } },
|
{ name: 'origin', refs: { fetch: 'http://my-repo.com' } },
|
||||||
@@ -189,6 +192,7 @@ describe('git extension helpers', () => {
|
|||||||
type: 'git',
|
type: 'git',
|
||||||
source: 'my/ext',
|
source: 'my/ext',
|
||||||
},
|
},
|
||||||
|
contextFiles: [],
|
||||||
};
|
};
|
||||||
mockGit.getRemotes.mockResolvedValue([
|
mockGit.getRemotes.mockResolvedValue([
|
||||||
{ name: 'origin', refs: { fetch: 'http://my-repo.com' } },
|
{ name: 'origin', refs: { fetch: 'http://my-repo.com' } },
|
||||||
@@ -210,6 +214,7 @@ describe('git extension helpers', () => {
|
|||||||
type: 'git',
|
type: 'git',
|
||||||
source: 'my/ext',
|
source: 'my/ext',
|
||||||
},
|
},
|
||||||
|
contextFiles: [],
|
||||||
};
|
};
|
||||||
mockGit.getRemotes.mockRejectedValue(new Error('git error'));
|
mockGit.getRemotes.mockRejectedValue(new Error('git error'));
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export async function checkForExtensionUpdate(
|
|||||||
);
|
);
|
||||||
return ExtensionUpdateState.ERROR;
|
return ExtensionUpdateState.ERROR;
|
||||||
}
|
}
|
||||||
if (newExtension.config.version !== extension.version) {
|
if (newExtension.version !== extension.version) {
|
||||||
return ExtensionUpdateState.UPDATE_AVAILABLE;
|
return ExtensionUpdateState.UPDATE_AVAILABLE;
|
||||||
}
|
}
|
||||||
return ExtensionUpdateState.UP_TO_DATE;
|
return ExtensionUpdateState.UP_TO_DATE;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export async function updateExtension(
|
|||||||
});
|
});
|
||||||
throw new Error('Updated extension not found after installation.');
|
throw new Error('Updated extension not found after installation.');
|
||||||
}
|
}
|
||||||
const updatedVersion = updatedExtension.config.version;
|
const updatedVersion = updatedExtension.version;
|
||||||
dispatchExtensionStateUpdate({
|
dispatchExtensionStateUpdate({
|
||||||
type: 'SET_STATE',
|
type: 'SET_STATE',
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ export async function main() {
|
|||||||
if (config.getListExtensions()) {
|
if (config.getListExtensions()) {
|
||||||
console.log('Installed extensions:');
|
console.log('Installed extensions:');
|
||||||
for (const extension of extensions) {
|
for (const extension of extensions) {
|
||||||
console.log(`- ${extension.config.name}`);
|
console.log(`- ${extension.name}`);
|
||||||
}
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box, Text, useInput } from 'ink';
|
import { Box, Text, useInput } from 'ink';
|
||||||
import {
|
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||||
type Extension,
|
import { performWorkspaceExtensionMigration } from '../../config/extension.js';
|
||||||
performWorkspaceExtensionMigration,
|
|
||||||
} from '../../config/extension.js';
|
|
||||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export function WorkspaceMigrationDialog(props: {
|
export function WorkspaceMigrationDialog(props: {
|
||||||
workspaceExtensions: Extension[];
|
workspaceExtensions: GeminiCLIExtension[];
|
||||||
onOpen: () => void;
|
onOpen: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -92,7 +90,7 @@ export function WorkspaceMigrationDialog(props: {
|
|||||||
|
|
||||||
<Box flexDirection="column" marginTop={1} marginLeft={2}>
|
<Box flexDirection="column" marginTop={1} marginLeft={2}>
|
||||||
{workspaceExtensions.map((extension) => (
|
{workspaceExtensions.map((extension) => (
|
||||||
<Text key={extension.config.name}>- {extension.config.name}</Text>
|
<Text key={extension.name}>- {extension.name}</Text>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ describe('useExtensionUpdates', () => {
|
|||||||
source: 'https://some/repo',
|
source: 'https://some/repo',
|
||||||
autoUpdate: false,
|
autoUpdate: false,
|
||||||
},
|
},
|
||||||
|
contextFiles: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const addItem = vi.fn();
|
const addItem = vi.fn();
|
||||||
@@ -262,6 +263,7 @@ describe('useExtensionUpdates', () => {
|
|||||||
source: 'https://some/repo1',
|
source: 'https://some/repo1',
|
||||||
autoUpdate: false,
|
autoUpdate: false,
|
||||||
},
|
},
|
||||||
|
contextFiles: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'test-extension-2',
|
name: 'test-extension-2',
|
||||||
@@ -274,6 +276,7 @@ describe('useExtensionUpdates', () => {
|
|||||||
source: 'https://some/repo2',
|
source: 'https://some/repo2',
|
||||||
autoUpdate: false,
|
autoUpdate: false,
|
||||||
},
|
},
|
||||||
|
contextFiles: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const addItem = vi.fn();
|
const addItem = vi.fn();
|
||||||
|
|||||||
@@ -5,19 +5,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||||
type Extension,
|
import { getWorkspaceExtensions } from '../../config/extension.js';
|
||||||
getWorkspaceExtensions,
|
|
||||||
} from '../../config/extension.js';
|
|
||||||
import { type LoadedSettings, SettingScope } from '../../config/settings.js';
|
import { type LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
|
||||||
export function useWorkspaceMigration(settings: LoadedSettings) {
|
export function useWorkspaceMigration(settings: LoadedSettings) {
|
||||||
const [showWorkspaceMigrationDialog, setShowWorkspaceMigrationDialog] =
|
const [showWorkspaceMigrationDialog, setShowWorkspaceMigrationDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [workspaceExtensions, setWorkspaceExtensions] = useState<Extension[]>(
|
const [workspaceExtensions, setWorkspaceExtensions] = useState<
|
||||||
[],
|
GeminiCLIExtension[]
|
||||||
);
|
>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Default to true if not set.
|
// Default to true if not set.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
GeminiChat,
|
GeminiChat,
|
||||||
ToolResult,
|
ToolResult,
|
||||||
ToolCallConfirmationDetails,
|
ToolCallConfirmationDetails,
|
||||||
|
GeminiCLIExtension,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
AuthType,
|
AuthType,
|
||||||
@@ -40,7 +41,7 @@ import * as path from 'node:path';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { ExtensionStorage, type Extension } from '../config/extension.js';
|
import { ExtensionStorage } from '../config/extension.js';
|
||||||
import type { CliArgs } from '../config/config.js';
|
import type { CliArgs } from '../config/config.js';
|
||||||
import { loadCliConfig } from '../config/config.js';
|
import { loadCliConfig } from '../config/config.js';
|
||||||
import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js';
|
import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js';
|
||||||
@@ -61,7 +62,7 @@ export function resolveModel(model: string, isInFallbackMode: boolean): string {
|
|||||||
export async function runZedIntegration(
|
export async function runZedIntegration(
|
||||||
config: Config,
|
config: Config,
|
||||||
settings: LoadedSettings,
|
settings: LoadedSettings,
|
||||||
extensions: Extension[],
|
extensions: GeminiCLIExtension[],
|
||||||
argv: CliArgs,
|
argv: CliArgs,
|
||||||
) {
|
) {
|
||||||
const stdout = Writable.toWeb(process.stdout) as WritableStream;
|
const stdout = Writable.toWeb(process.stdout) as WritableStream;
|
||||||
@@ -88,7 +89,7 @@ class GeminiAgent {
|
|||||||
constructor(
|
constructor(
|
||||||
private config: Config,
|
private config: Config,
|
||||||
private settings: LoadedSettings,
|
private settings: LoadedSettings,
|
||||||
private extensions: Extension[],
|
private extensions: GeminiCLIExtension[],
|
||||||
private argv: CliArgs,
|
private argv: CliArgs,
|
||||||
private client: acp.Client,
|
private client: acp.Client,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -116,12 +116,21 @@ export interface OutputSettings {
|
|||||||
format?: OutputFormat;
|
format?: OutputFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All information required in CLI to handle an extension. Defined in Core so
|
||||||
|
* that the collection of loaded, active, and inactive extensions can be passed
|
||||||
|
* around on the config object though Core does not use this information
|
||||||
|
* directly.
|
||||||
|
*/
|
||||||
export interface GeminiCLIExtension {
|
export interface GeminiCLIExtension {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
installMetadata?: ExtensionInstallMetadata;
|
installMetadata?: ExtensionInstallMetadata;
|
||||||
|
mcpServers?: Record<string, MCPServerConfig>;
|
||||||
|
contextFiles: string[];
|
||||||
|
excludeTools?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionInstallMetadata {
|
export interface ExtensionInstallMetadata {
|
||||||
|
|||||||
Reference in New Issue
Block a user