2025-10-23 11:39:36 -07:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs' ;
import * as path from 'node:path' ;
import * as os from 'node:os' ;
import chalk from 'chalk' ;
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js' ;
2025-10-28 09:04:30 -07:00
import { type Settings , SettingScope } from './settings.js' ;
2025-10-23 11:39:36 -07:00
import { createHash , randomUUID } from 'node:crypto' ;
import { loadInstallMetadata , type ExtensionConfig } from './extension.js' ;
2025-10-31 19:17:01 +00:00
import {
isWorkspaceTrusted ,
loadTrustedFolders ,
TrustLevel ,
} from './trustedFolders.js' ;
2025-10-23 11:39:36 -07:00
import {
cloneFromGit ,
downloadFromGitHubRelease ,
tryParseGithubUrl ,
} from './extensions/github.js' ;
import {
Config ,
debugLogger ,
ExtensionDisableEvent ,
ExtensionEnableEvent ,
ExtensionInstallEvent ,
ExtensionUninstallEvent ,
ExtensionUpdateEvent ,
getErrorMessage ,
logExtensionDisable ,
logExtensionEnable ,
logExtensionInstallEvent ,
logExtensionUninstall ,
logExtensionUpdateEvent ,
type MCPServerConfig ,
type ExtensionInstallMetadata ,
type GeminiCLIExtension ,
} from '@google/gemini-cli-core' ;
import { maybeRequestConsentOrFail } from './extensions/consent.js' ;
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js' ;
import { ExtensionStorage } from './extensions/storage.js' ;
import {
EXTENSIONS_CONFIG_FILENAME ,
INSTALL_METADATA_FILENAME ,
recursivelyHydrateStrings ,
type JsonObject ,
} from './extensions/variables.js' ;
import {
getEnvContents ,
maybePromptForSettings ,
type ExtensionSetting ,
} from './extensions/extensionSettings.js' ;
2025-10-28 09:04:30 -07:00
import type {
ExtensionEvents ,
ExtensionLoader ,
} from '@google/gemini-cli-core/src/utils/extensionLoader.js' ;
import { EventEmitter } from 'node:events' ;
2025-10-23 11:39:36 -07:00
interface ExtensionManagerParams {
enabledExtensionOverrides? : string [ ] ;
2025-10-28 09:04:30 -07:00
settings : Settings ;
2025-10-23 11:39:36 -07:00
requestConsent : ( consent : string ) = > Promise < boolean > ;
requestSetting : ( ( setting : ExtensionSetting ) = > Promise < string > ) | null ;
workspaceDir : string ;
}
2025-10-28 09:04:30 -07:00
/**
* Actual implementation of an ExtensionLoader.
*
* You must call `loadExtensions` prior to calling other methods on this class.
*/
export class ExtensionManager implements ExtensionLoader {
2025-10-23 11:39:36 -07:00
private extensionEnablementManager : ExtensionEnablementManager ;
2025-10-28 09:04:30 -07:00
private settings : Settings ;
2025-10-23 11:39:36 -07:00
private requestConsent : ( consent : string ) = > Promise < boolean > ;
private requestSetting :
| ( ( setting : ExtensionSetting ) = > Promise < string > )
2025-10-28 09:04:30 -07:00
| undefined ;
2025-10-23 11:39:36 -07:00
private telemetryConfig : Config ;
private workspaceDir : string ;
2025-10-28 09:04:30 -07:00
private loadedExtensions : GeminiCLIExtension [ ] | undefined ;
private eventEmitter : EventEmitter < ExtensionEvents > ;
2025-10-23 11:39:36 -07:00
constructor ( options : ExtensionManagerParams ) {
this . workspaceDir = options . workspaceDir ;
this . extensionEnablementManager = new ExtensionEnablementManager (
options . enabledExtensionOverrides ,
) ;
2025-10-28 09:04:30 -07:00
this . settings = options . settings ;
2025-10-23 11:39:36 -07:00
this . telemetryConfig = new Config ( {
2025-10-28 09:04:30 -07:00
telemetry : options.settings.telemetry ,
2025-10-23 11:39:36 -07:00
interactive : false ,
sessionId : randomUUID ( ) ,
targetDir : options.workspaceDir ,
cwd : options.workspaceDir ,
model : '' ,
debugMode : false ,
} ) ;
this . requestConsent = options . requestConsent ;
2025-10-28 09:04:30 -07:00
this . requestSetting = options . requestSetting ? ? undefined ;
this . eventEmitter = new EventEmitter ( ) ;
}
setRequestConsent (
requestConsent : ( consent : string ) = > Promise < boolean > ,
) : void {
this . requestConsent = requestConsent ;
}
setRequestSetting (
requestSetting ? : ( setting : ExtensionSetting ) = > Promise < string > ,
) : void {
this . requestSetting = requestSetting ;
}
getExtensions ( ) : GeminiCLIExtension [ ] {
if ( ! this . loadedExtensions ) {
throw new Error (
'Extensions not yet loaded, must call `loadExtensions` first' ,
) ;
}
return this . loadedExtensions ! ;
}
extensionEvents ( ) : EventEmitter < ExtensionEvents > {
return this . eventEmitter ;
2025-10-23 11:39:36 -07:00
}
async installOrUpdateExtension (
installMetadata : ExtensionInstallMetadata ,
previousExtensionConfig? : ExtensionConfig ,
2025-10-28 09:04:30 -07:00
) : Promise < GeminiCLIExtension > {
2025-10-23 11:39:36 -07:00
const isUpdate = ! ! previousExtensionConfig ;
let newExtensionConfig : ExtensionConfig | null = null ;
let localSourcePath : string | undefined ;
2025-10-28 14:48:50 -04:00
let extension : GeminiCLIExtension | null ;
2025-10-23 11:39:36 -07:00
try {
2025-10-28 09:04:30 -07:00
if ( ! isWorkspaceTrusted ( this . settings ) . isTrusted ) {
2025-10-31 19:17:01 +00:00
if (
await this . requestConsent (
` The current workspace at " ${ this . workspaceDir } " is not trusted. Do you want to trust this workspace to install extensions? ` ,
)
) {
const trustedFolders = loadTrustedFolders ( ) ;
trustedFolders . setValue ( this . workspaceDir , TrustLevel . TRUST_FOLDER ) ;
} else {
throw new Error (
` Could not install extension because the current workspace at ${ this . workspaceDir } is not trusted. ` ,
) ;
}
2025-10-23 11:39:36 -07:00
}
const extensionsDir = ExtensionStorage . getUserExtensionsDir ( ) ;
await fs . promises . mkdir ( extensionsDir , { recursive : true } ) ;
if (
! path . isAbsolute ( installMetadata . source ) &&
( installMetadata . type === 'local' || installMetadata . type === 'link' )
) {
installMetadata . source = path . resolve (
this . workspaceDir ,
installMetadata . source ,
) ;
}
let tempDir : string | undefined ;
if (
installMetadata . type === 'git' ||
installMetadata . type === 'github-release'
) {
tempDir = await ExtensionStorage . createTmpDir ( ) ;
const parsedGithubParts = tryParseGithubUrl ( installMetadata . source ) ;
if ( ! parsedGithubParts ) {
await cloneFromGit ( installMetadata , tempDir ) ;
installMetadata . type = 'git' ;
} else {
const result = await downloadFromGitHubRelease (
installMetadata ,
tempDir ,
parsedGithubParts ,
) ;
if ( result . success ) {
installMetadata . type = result . type ;
installMetadata . releaseTag = result . tagName ;
} else if (
// This repo has no github releases, and wasn't explicitly installed
// from a github release, unconditionally just clone it.
( result . failureReason === 'no release data' &&
installMetadata . type === 'git' ) ||
// Otherwise ask the user if they would like to try a git clone.
( await this . requestConsent (
` Error downloading github release for ${ installMetadata . source } with the following error: ${ result . errorMessage } . \ n \ nWould you like to attempt to install via "git clone" instead? ` ,
) )
) {
await cloneFromGit ( installMetadata , tempDir ) ;
installMetadata . type = 'git' ;
} else {
throw new Error (
` Failed to install extension ${ installMetadata . source } : ${ result . errorMessage } ` ,
) ;
}
}
localSourcePath = tempDir ;
} else if (
installMetadata . type === 'local' ||
installMetadata . type === 'link'
) {
localSourcePath = installMetadata . source ;
} else {
throw new Error ( ` Unsupported install type: ${ installMetadata . type } ` ) ;
}
try {
newExtensionConfig = this . loadExtensionConfig ( localSourcePath ) ;
if ( isUpdate && installMetadata . autoUpdate ) {
const oldSettings = new Set (
previousExtensionConfig . settings ? . map ( ( s ) = > s . name ) || [ ] ,
) ;
const newSettings = new Set (
newExtensionConfig . settings ? . map ( ( s ) = > s . name ) || [ ] ,
) ;
const settingsAreEqual =
oldSettings . size === newSettings . size &&
[ . . . oldSettings ] . every ( ( value ) = > newSettings . has ( value ) ) ;
if ( ! settingsAreEqual && installMetadata . autoUpdate ) {
throw new Error (
` Extension " ${ newExtensionConfig . name } " has settings changes and cannot be auto-updated. Please update manually. ` ,
) ;
}
}
const newExtensionName = newExtensionConfig . name ;
2025-10-28 09:04:30 -07:00
const previous = this . getExtensions ( ) . find (
( installed ) = > installed . name === newExtensionName ,
) ;
if ( isUpdate && ! previous ) {
throw new Error (
` Extension " ${ newExtensionName } " was not already installed, cannot update it. ` ,
) ;
} else if ( ! isUpdate && previous ) {
throw new Error (
` Extension " ${ newExtensionName } " is already installed. Please uninstall it first. ` ,
) ;
2025-10-23 11:39:36 -07:00
}
await maybeRequestConsentOrFail (
newExtensionConfig ,
this . requestConsent ,
previousExtensionConfig ,
) ;
2025-10-28 14:48:50 -04:00
const extensionId = getExtensionId ( newExtensionConfig , installMetadata ) ;
const destinationPath = new ExtensionStorage (
newExtensionName ,
) . getExtensionDir ( ) ;
2025-10-23 11:39:36 -07:00
let previousSettings : Record < string , string > | undefined ;
if ( isUpdate ) {
2025-10-28 14:48:50 -04:00
previousSettings = await getEnvContents (
previousExtensionConfig ,
extensionId ,
) ;
2025-10-23 11:39:36 -07:00
await this . uninstallExtension ( newExtensionName , isUpdate ) ;
}
await fs . promises . mkdir ( destinationPath , { recursive : true } ) ;
if ( this . requestSetting ) {
if ( isUpdate ) {
await maybePromptForSettings (
newExtensionConfig ,
2025-10-28 14:48:50 -04:00
extensionId ,
2025-10-23 11:39:36 -07:00
this . requestSetting ,
previousExtensionConfig ,
previousSettings ,
) ;
} else {
await maybePromptForSettings (
newExtensionConfig ,
2025-10-28 14:48:50 -04:00
extensionId ,
2025-10-23 11:39:36 -07:00
this . requestSetting ,
) ;
}
}
if (
installMetadata . type === 'local' ||
installMetadata . type === 'git' ||
installMetadata . type === 'github-release'
) {
await copyExtension ( localSourcePath , destinationPath ) ;
}
const metadataString = JSON . stringify ( installMetadata , null , 2 ) ;
const metadataPath = path . join (
destinationPath ,
INSTALL_METADATA_FILENAME ,
) ;
await fs . promises . writeFile ( metadataPath , metadataString ) ;
2025-10-28 09:04:30 -07:00
// TODO: Gracefully handle this call failing, we should back up the old
// extension prior to overwriting it and then restore it.
2025-10-28 14:48:50 -04:00
extension = await this . loadExtension ( destinationPath ) ! ;
if ( ! extension ) {
throw new Error ( ` Extension not found ` ) ;
}
2025-10-28 09:04:30 -07:00
if ( isUpdate ) {
logExtensionUpdateEvent (
this . telemetryConfig ,
new ExtensionUpdateEvent (
hashValue ( newExtensionConfig . name ) ,
getExtensionId ( newExtensionConfig , installMetadata ) ,
newExtensionConfig . version ,
previousExtensionConfig . version ,
installMetadata . type ,
'success' ,
) ,
) ;
this . eventEmitter . emit ( 'extensionUpdated' , { extension } ) ;
} else {
logExtensionInstallEvent (
this . telemetryConfig ,
new ExtensionInstallEvent (
hashValue ( newExtensionConfig . name ) ,
getExtensionId ( newExtensionConfig , installMetadata ) ,
newExtensionConfig . version ,
installMetadata . type ,
'success' ,
) ,
) ;
this . eventEmitter . emit ( 'extensionInstalled' , { extension } ) ;
this . enableExtension ( newExtensionConfig . name , SettingScope . User ) ;
}
2025-10-23 11:39:36 -07:00
} finally {
if ( tempDir ) {
await fs . promises . rm ( tempDir , { recursive : true , force : true } ) ;
}
}
2025-10-28 09:04:30 -07:00
return extension ;
2025-10-23 11:39:36 -07:00
} catch ( error ) {
// Attempt to load config from the source path even if installation fails
// to get the name and version for logging.
if ( ! newExtensionConfig && localSourcePath ) {
try {
newExtensionConfig = this . loadExtensionConfig ( localSourcePath ) ;
} catch {
// Ignore error, this is just for logging.
}
}
const config = newExtensionConfig ? ? previousExtensionConfig ;
const extensionId = config
? getExtensionId ( config , installMetadata )
: undefined ;
if ( isUpdate ) {
logExtensionUpdateEvent (
this . telemetryConfig ,
new ExtensionUpdateEvent (
hashValue ( config ? . name ? ? '' ) ,
extensionId ? ? '' ,
newExtensionConfig ? . version ? ? '' ,
previousExtensionConfig . version ,
installMetadata . type ,
'error' ,
) ,
) ;
} else {
logExtensionInstallEvent (
this . telemetryConfig ,
new ExtensionInstallEvent (
hashValue ( newExtensionConfig ? . name ? ? '' ) ,
extensionId ? ? '' ,
newExtensionConfig ? . version ? ? '' ,
installMetadata . type ,
'error' ,
) ,
) ;
}
throw error ;
}
}
async uninstallExtension (
extensionIdentifier : string ,
isUpdate : boolean ,
) : Promise < void > {
2025-10-28 09:04:30 -07:00
const installedExtensions = this . getExtensions ( ) ;
2025-10-23 11:39:36 -07:00
const extension = installedExtensions . find (
( installed ) = >
installed . name . toLowerCase ( ) === extensionIdentifier . toLowerCase ( ) ||
installed . installMetadata ? . source . toLowerCase ( ) ===
extensionIdentifier . toLowerCase ( ) ,
) ;
if ( ! extension ) {
throw new Error ( ` Extension not found. ` ) ;
}
2025-10-28 09:04:30 -07:00
this . unloadExtension ( extension ) ;
2025-10-23 11:39:36 -07:00
const storage = new ExtensionStorage ( extension . name ) ;
await fs . promises . rm ( storage . getExtensionDir ( ) , {
recursive : true ,
force : true ,
} ) ;
// The rest of the cleanup below here is only for true uninstalls, not
// uninstalls related to updates.
if ( isUpdate ) return ;
this . extensionEnablementManager . remove ( extension . name ) ;
logExtensionUninstall (
this . telemetryConfig ,
new ExtensionUninstallEvent (
hashValue ( extension . name ) ,
extension . id ,
'success' ,
) ,
) ;
2025-10-28 09:04:30 -07:00
this . eventEmitter . emit ( 'extensionUninstalled' , { extension } ) ;
2025-10-23 11:39:36 -07:00
}
2025-10-28 14:48:50 -04:00
async loadExtensions ( ) : Promise < GeminiCLIExtension [ ] > {
2025-10-28 09:04:30 -07:00
if ( this . loadedExtensions ) {
throw new Error ( 'Extensions already loaded, only load extensions once.' ) ;
}
2025-10-23 11:39:36 -07:00
const extensionsDir = ExtensionStorage . getUserExtensionsDir ( ) ;
2025-10-28 09:04:30 -07:00
this . loadedExtensions = [ ] ;
2025-10-23 11:39:36 -07:00
if ( ! fs . existsSync ( extensionsDir ) ) {
2025-10-28 09:04:30 -07:00
return this . loadedExtensions ;
2025-10-23 11:39:36 -07:00
}
for ( const subdir of fs . readdirSync ( extensionsDir ) ) {
const extensionDir = path . join ( extensionsDir , subdir ) ;
2025-10-28 14:48:50 -04:00
await this . loadExtension ( extensionDir ) ;
2025-10-23 11:39:36 -07:00
}
2025-10-28 09:04:30 -07:00
return this . loadedExtensions ;
2025-10-23 11:39:36 -07:00
}
2025-10-28 14:48:50 -04:00
private async loadExtension (
extensionDir : string ,
) : Promise < GeminiCLIExtension | null > {
2025-10-28 09:04:30 -07:00
this . loadedExtensions ? ? = [ ] ;
2025-10-23 11:39:36 -07:00
if ( ! fs . statSync ( extensionDir ) . isDirectory ( ) ) {
return null ;
}
const installMetadata = loadInstallMetadata ( extensionDir ) ;
let effectiveExtensionPath = extensionDir ;
if ( installMetadata ? . type === 'link' ) {
effectiveExtensionPath = installMetadata . source ;
}
try {
let config = this . loadExtensionConfig ( effectiveExtensionPath ) ;
2025-10-28 09:04:30 -07:00
if (
this . getExtensions ( ) . find ( ( extension ) = > extension . name === config . name )
) {
throw new Error (
` Extension with name ${ config . name } already was loaded. ` ,
) ;
}
2025-10-23 11:39:36 -07:00
2025-10-28 14:48:50 -04:00
const customEnv = await getEnvContents (
config ,
getExtensionId ( config , installMetadata ) ,
) ;
2025-10-23 11:39:36 -07:00
config = resolveEnvVarsInObject ( config , customEnv ) ;
if ( config . mcpServers ) {
config . mcpServers = Object . fromEntries (
Object . entries ( config . mcpServers ) . map ( ( [ key , value ] ) = > [
key ,
filterMcpConfig ( value ) ,
] ) ,
) ;
}
const contextFiles = getContextFileNames ( config )
. map ( ( contextFileName ) = >
path . join ( effectiveExtensionPath , contextFileName ) ,
)
. filter ( ( contextFilePath ) = > fs . existsSync ( contextFilePath ) ) ;
2025-10-28 09:04:30 -07:00
const extension = {
2025-10-23 11:39:36 -07:00
name : config.name ,
version : config.version ,
path : effectiveExtensionPath ,
contextFiles ,
installMetadata ,
mcpServers : config.mcpServers ,
excludeTools : config.excludeTools ,
isActive : this.extensionEnablementManager.isEnabled (
config . name ,
this . workspaceDir ,
) ,
id : getExtensionId ( config , installMetadata ) ,
} ;
2025-10-28 09:04:30 -07:00
this . eventEmitter . emit ( 'extensionLoaded' , { extension } ) ;
this . getExtensions ( ) . push ( extension ) ;
return extension ;
2025-10-23 11:39:36 -07:00
} catch ( e ) {
debugLogger . error (
` Warning: Skipping extension in ${ effectiveExtensionPath } : ${ getErrorMessage (
e ,
) } ` ,
) ;
return null ;
}
}
2025-10-28 09:04:30 -07:00
private unloadExtension ( extension : GeminiCLIExtension ) {
this . loadedExtensions = this . getExtensions ( ) . filter (
( entry ) = > extension !== entry ,
) ;
this . eventEmitter . emit ( 'extensionUnloaded' , { extension } ) ;
2025-10-23 11:39:36 -07:00
}
loadExtensionConfig ( extensionDir : string ) : ExtensionConfig {
const configFilePath = path . join ( extensionDir , EXTENSIONS_CONFIG_FILENAME ) ;
if ( ! fs . existsSync ( configFilePath ) ) {
throw new Error ( ` Configuration file not found at ${ configFilePath } ` ) ;
}
try {
const configContent = fs . readFileSync ( configFilePath , 'utf-8' ) ;
const rawConfig = JSON . parse ( configContent ) as ExtensionConfig ;
if ( ! rawConfig . name || ! rawConfig . version ) {
throw new Error (
` Invalid configuration in ${ configFilePath } : missing ${ ! rawConfig . name ? '"name"' : '"version"' } ` ,
) ;
}
const config = recursivelyHydrateStrings (
rawConfig as unknown as JsonObject ,
{
2025-10-24 10:45:58 -07:00
extensionPath : extensionDir ,
2025-10-23 11:39:36 -07:00
workspacePath : this.workspaceDir ,
'/' : path . sep ,
pathSeparator : path.sep ,
} ,
) as unknown as ExtensionConfig ;
validateName ( config . name ) ;
return config ;
} catch ( e ) {
throw new Error (
` Failed to load extension config from ${ configFilePath } : ${ getErrorMessage (
e ,
) } ` ,
) ;
}
}
toOutputString ( extension : GeminiCLIExtension ) : string {
const userEnabled = this . extensionEnablementManager . isEnabled (
extension . name ,
os . homedir ( ) ,
) ;
const workspaceEnabled = this . extensionEnablementManager . isEnabled (
extension . name ,
this . workspaceDir ,
) ;
const status = workspaceEnabled ? chalk . green ( '✓' ) : chalk . red ( '✗' ) ;
let output = ` ${ status } ${ extension . name } ( ${ extension . version } ) ` ;
output += ` \ n ID: ${ extension . id } ` ;
output += ` \ n Path: ${ extension . path } ` ;
if ( extension . installMetadata ) {
output += ` \ n Source: ${ extension . installMetadata . source } (Type: ${ extension . installMetadata . type } ) ` ;
if ( extension . installMetadata . ref ) {
output += ` \ n Ref: ${ extension . installMetadata . ref } ` ;
}
if ( extension . installMetadata . releaseTag ) {
output += ` \ n Release tag: ${ extension . installMetadata . releaseTag } ` ;
}
}
output += ` \ n Enabled (User): ${ userEnabled } ` ;
output += ` \ n Enabled (Workspace): ${ workspaceEnabled } ` ;
if ( extension . contextFiles . length > 0 ) {
output += ` \ n Context files: ` ;
extension . contextFiles . forEach ( ( contextFile ) = > {
output += ` \ n ${ contextFile } ` ;
} ) ;
}
if ( extension . mcpServers ) {
output += ` \ n MCP servers: ` ;
Object . keys ( extension . mcpServers ) . forEach ( ( key ) = > {
output += ` \ n ${ key } ` ;
} ) ;
}
if ( extension . excludeTools ) {
output += ` \ n Excluded tools: ` ;
extension . excludeTools . forEach ( ( tool ) = > {
output += ` \ n ${ tool } ` ;
} ) ;
}
return output ;
}
2025-10-28 14:48:50 -04:00
async disableExtension ( name : string , scope : SettingScope ) {
2025-10-23 11:39:36 -07:00
if (
scope === SettingScope . System ||
scope === SettingScope . SystemDefaults
) {
throw new Error ( 'System and SystemDefaults scopes are not supported.' ) ;
}
2025-10-28 09:04:30 -07:00
const extension = this . getExtensions ( ) . find (
( extension ) = > extension . name === name ,
) ;
2025-10-23 11:39:36 -07:00
if ( ! extension ) {
throw new Error ( ` Extension with name ${ name } does not exist. ` ) ;
}
const scopePath =
scope === SettingScope . Workspace ? this . workspaceDir : os.homedir ( ) ;
this . extensionEnablementManager . disable ( name , true , scopePath ) ;
logExtensionDisable (
this . telemetryConfig ,
new ExtensionDisableEvent ( hashValue ( name ) , extension . id , scope ) ,
) ;
2025-10-28 09:04:30 -07:00
extension . isActive = false ;
this . eventEmitter . emit ( 'extensionDisabled' , { extension } ) ;
2025-10-23 11:39:36 -07:00
}
2025-10-28 14:48:50 -04:00
async enableExtension ( name : string , scope : SettingScope ) {
2025-10-23 11:39:36 -07:00
if (
scope === SettingScope . System ||
scope === SettingScope . SystemDefaults
) {
throw new Error ( 'System and SystemDefaults scopes are not supported.' ) ;
}
2025-10-28 09:04:30 -07:00
const extension = this . getExtensions ( ) . find (
( extension ) = > extension . name === name ,
) ;
2025-10-23 11:39:36 -07:00
if ( ! extension ) {
throw new Error ( ` Extension with name ${ name } does not exist. ` ) ;
}
const scopePath =
scope === SettingScope . Workspace ? this . workspaceDir : os.homedir ( ) ;
this . extensionEnablementManager . enable ( name , true , scopePath ) ;
logExtensionEnable (
this . telemetryConfig ,
new ExtensionEnableEvent ( hashValue ( name ) , extension . id , scope ) ,
) ;
2025-10-28 09:04:30 -07:00
extension . isActive = true ;
this . eventEmitter . emit ( 'extensionEnabled' , { extension } ) ;
2025-10-23 11:39:36 -07:00
}
}
function filterMcpConfig ( original : MCPServerConfig ) : MCPServerConfig {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { trust , . . . rest } = original ;
return Object . freeze ( rest ) ;
}
export async function copyExtension (
source : string ,
destination : string ,
) : Promise < void > {
await fs . promises . cp ( source , destination , { recursive : true } ) ;
}
function getContextFileNames ( config : ExtensionConfig ) : string [ ] {
if ( ! config . contextFileName ) {
return [ 'GEMINI.md' ] ;
} else if ( ! Array . isArray ( config . contextFileName ) ) {
return [ config . contextFileName ] ;
}
return config . contextFileName ;
}
function validateName ( name : string ) {
if ( ! /^[a-zA-Z0-9-]+$/ . test ( name ) ) {
throw new Error (
` Invalid extension name: " ${ name } ". Only letters (a-z, A-Z), numbers (0-9), and dashes (-) are allowed. ` ,
) ;
}
}
export function getExtensionId (
config : ExtensionConfig ,
installMetadata? : ExtensionInstallMetadata ,
) : string {
// IDs are created by hashing details of the installation source in order to
// deduplicate extensions with conflicting names and also obfuscate any
// potentially sensitive information such as private git urls, system paths,
// or project names.
let idValue = config . name ;
const githubUrlParts =
installMetadata &&
( installMetadata . type === 'git' ||
installMetadata . type === 'github-release' )
? tryParseGithubUrl ( installMetadata . source )
: null ;
if ( githubUrlParts ) {
// For github repos, we use the https URI to the repo as the ID.
idValue = ` https://github.com/ ${ githubUrlParts . owner } / ${ githubUrlParts . repo } ` ;
} else {
idValue = installMetadata ? . source ? ? config . name ;
}
return hashValue ( idValue ) ;
}
export function hashValue ( value : string ) : string {
return createHash ( 'sha256' ) . update ( value ) . digest ( 'hex' ) ;
}