| |
| |
| |
| |
| |
|
|
| import { |
| debugLogger, |
| listExtensions, |
| getErrorMessage, |
| type ExtensionInstallMetadata, |
| } from '@google/gemini-cli-core'; |
| import type { ExtensionUpdateInfo } from '../../config/extension.js'; |
| import { |
| emptyIcon, |
| MessageType, |
| type HistoryItemExtensionsList, |
| type HistoryItemInfo, |
| } from '../types.js'; |
| import { |
| type CommandContext, |
| type SlashCommand, |
| type SlashCommandActionReturn, |
| CommandKind, |
| } from './types.js'; |
| import open from 'open'; |
| import process from 'node:process'; |
| import { |
| ExtensionManager, |
| inferInstallMetadata, |
| } from '../../config/extension-manager.js'; |
| import { SettingScope } from '../../config/settings.js'; |
| import { McpServerEnablementManager } from '../../config/mcp/mcpServerEnablement.js'; |
| import { theme } from '../semantic-colors.js'; |
| import { stat } from 'node:fs/promises'; |
| import { ExtensionSettingScope } from '../../config/extensions/extensionSettings.js'; |
| import { type ConfigLogger } from '../../commands/extensions/utils.js'; |
| import { ConfigExtensionDialog } from '../components/ConfigExtensionDialog.js'; |
| import { ExtensionRegistryView } from '../components/views/ExtensionRegistryView.js'; |
| import React from 'react'; |
|
|
| function showMessageIfNoExtensions( |
| context: CommandContext, |
| extensions: unknown[], |
| ): boolean { |
| if (extensions.length === 0) { |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: 'No extensions installed. Run `/extensions explore` to check out the gallery.', |
| }); |
| return true; |
| } |
| return false; |
| } |
|
|
| async function listAction(context: CommandContext) { |
| const extensions = context.services.agentContext?.config |
| ? listExtensions(context.services.agentContext.config) |
| : []; |
|
|
| if (showMessageIfNoExtensions(context, extensions)) { |
| return; |
| } |
|
|
| const historyItem: HistoryItemExtensionsList = { |
| type: MessageType.EXTENSIONS_LIST, |
| extensions, |
| }; |
|
|
| context.ui.addItem(historyItem); |
| } |
|
|
| function updateAction(context: CommandContext, args: string): Promise<void> { |
| const updateArgs = args.split(' ').filter((value) => value.length > 0); |
| const all = updateArgs.length === 1 && updateArgs[0] === '--all'; |
| const names = all ? null : updateArgs; |
|
|
| if (!all && names?.length === 0) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: 'Usage: /extensions update <extension-names>|--all', |
| }); |
| return Promise.resolve(); |
| } |
|
|
| let resolveUpdateComplete: (updateInfo: ExtensionUpdateInfo[]) => void; |
| const updateComplete = new Promise<ExtensionUpdateInfo[]>( |
| (resolve) => (resolveUpdateComplete = resolve), |
| ); |
|
|
| const extensions = context.services.agentContext?.config |
| ? listExtensions(context.services.agentContext.config) |
| : []; |
|
|
| if (showMessageIfNoExtensions(context, extensions)) { |
| return Promise.resolve(); |
| } |
|
|
| const historyItem: HistoryItemExtensionsList = { |
| type: MessageType.EXTENSIONS_LIST, |
| extensions, |
| }; |
|
|
| |
| updateComplete.then((updateInfos) => { |
| if (updateInfos.length === 0) { |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: 'No extensions to update.', |
| }); |
| } |
|
|
| context.ui.addItem(historyItem); |
| context.ui.setPendingItem(null); |
| }); |
|
|
| try { |
| context.ui.setPendingItem(historyItem); |
|
|
| context.ui.dispatchExtensionStateUpdate({ |
| type: 'SCHEDULE_UPDATE', |
| payload: { |
| all, |
| names, |
| onComplete: (updateInfos) => { |
| resolveUpdateComplete(updateInfos); |
| }, |
| }, |
| }); |
| if (names?.length) { |
| const extensions = listExtensions(context.services.agentContext!.config); |
| for (const name of names) { |
| const extension = extensions.find( |
| (extension) => extension.name === name, |
| ); |
| if (!extension) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Extension ${name} not found.`, |
| }); |
| continue; |
| } |
| } |
| } |
| } catch (error) { |
| resolveUpdateComplete!([]); |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: getErrorMessage(error), |
| }); |
| } |
| return updateComplete.then((_) => {}); |
| } |
|
|
| async function restartAction( |
| context: CommandContext, |
| args: string, |
| ): Promise<void> { |
| const extensionLoader = |
| context.services.agentContext?.config.getExtensionLoader(); |
| if (!extensionLoader) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: "Extensions are not yet loaded, can't restart yet", |
| }); |
| return; |
| } |
|
|
| const extensions = extensionLoader.getExtensions(); |
| if (showMessageIfNoExtensions(context, extensions)) { |
| return; |
| } |
|
|
| const restartArgs = args.split(' ').filter((value) => value.length > 0); |
| const all = restartArgs.length === 1 && restartArgs[0] === '--all'; |
| const names = all ? null : restartArgs; |
| if (!all && names?.length === 0) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: 'Usage: /extensions reload <extension-names>|--all', |
| }); |
| return Promise.resolve(); |
| } |
|
|
| let extensionsToRestart = extensionLoader |
| .getExtensions() |
| .filter((extension) => extension.isActive); |
| if (names) { |
| extensionsToRestart = extensionsToRestart.filter((extension) => |
| names.includes(extension.name), |
| ); |
| if (names.length !== extensionsToRestart.length) { |
| const notFound = names.filter( |
| (name) => |
| !extensionsToRestart.some((extension) => extension.name === name), |
| ); |
| if (notFound.length > 0) { |
| context.ui.addItem({ |
| type: MessageType.WARNING, |
| text: `Extension(s) not found or not active: ${notFound.join(', ')}`, |
| }); |
| } |
| } |
| } |
| if (extensionsToRestart.length === 0) { |
| |
| return; |
| } |
|
|
| const s = extensionsToRestart.length > 1 ? 's' : ''; |
|
|
| const reloadingMessage = { |
| type: MessageType.INFO, |
| text: `Reloading ${extensionsToRestart.length} extension${s}...`, |
| color: theme.text.primary, |
| }; |
| context.ui.addItem(reloadingMessage); |
|
|
| const results = await Promise.allSettled( |
| extensionsToRestart.map(async (extension) => { |
| if (extension.isActive) { |
| await extensionLoader.restartExtension(extension); |
| context.ui.dispatchExtensionStateUpdate({ |
| type: 'RESTARTED', |
| payload: { |
| name: extension.name, |
| }, |
| }); |
| } |
| }), |
| ); |
|
|
| const failures = results.filter( |
| (result): result is PromiseRejectedResult => result.status === 'rejected', |
| ); |
|
|
| if (failures.length < extensionsToRestart.length) { |
| try { |
| await context.services.agentContext?.config.reloadSkills(); |
| await context.services.agentContext?.config.getAgentRegistry()?.reload(); |
| } catch (error) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Failed to reload skills or agents: ${getErrorMessage(error)}`, |
| }); |
| } |
| } |
|
|
| if (failures.length > 0) { |
| const errorMessages = failures |
| .map((failure, index) => { |
| const extensionName = extensionsToRestart[index].name; |
| return `${extensionName}: ${getErrorMessage(failure.reason)}`; |
| }) |
| .join('\n '); |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Failed to reload some extensions:\n ${errorMessages}`, |
| }); |
| } else { |
| const infoItem: HistoryItemInfo = { |
| type: MessageType.INFO, |
| text: `${extensionsToRestart.length} extension${s} reloaded successfully`, |
| icon: emptyIcon, |
| color: theme.text.primary, |
| }; |
| context.ui.addItem(infoItem); |
| } |
| } |
|
|
| async function exploreAction( |
| context: CommandContext, |
| ): Promise<SlashCommandActionReturn | void> { |
| const settings = context.services.settings.merged; |
| const useRegistryUI = settings.experimental?.extensionRegistry; |
|
|
| if (useRegistryUI) { |
| const extensionManager = |
| context.services.agentContext?.config.getExtensionLoader(); |
| if (extensionManager instanceof ExtensionManager) { |
| return { |
| type: 'custom_dialog' as const, |
| component: React.createElement(ExtensionRegistryView, { |
| onSelect: async (extension, requestConsentOverride) => { |
| debugLogger.log(`Selected extension: ${extension.extensionName}`); |
| await installAction(context, extension.url, requestConsentOverride); |
| context.ui.removeComponent(); |
| }, |
| onLink: async (extension, requestConsentOverride) => { |
| debugLogger.log(`Linking extension: ${extension.extensionName}`); |
| await linkAction(context, extension.url, requestConsentOverride); |
| context.ui.removeComponent(); |
| }, |
| onClose: () => context.ui.removeComponent(), |
| extensionManager, |
| }), |
| }; |
| } |
| } |
|
|
| const extensionsUrl = 'https://geminicli.com/extensions/'; |
|
|
| |
| if (process.env['NODE_ENV'] === 'test') { |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Would open extensions page in your browser: ${extensionsUrl} (skipped in test environment)`, |
| }); |
| } else if ( |
| process.env['SANDBOX'] && |
| process.env['SANDBOX'] !== 'sandbox-exec' |
| ) { |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `View available extensions at ${extensionsUrl}`, |
| }); |
| } else { |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Opening extensions page in your browser: ${extensionsUrl}`, |
| }); |
| try { |
| await open(extensionsUrl); |
| } catch { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Failed to open browser. Check out the extensions gallery at ${extensionsUrl}`, |
| }); |
| } |
| } |
| } |
|
|
| function getEnableDisableContext( |
| context: CommandContext, |
| argumentsString: string, |
| ): { |
| extensionManager: ExtensionManager; |
| names: string[]; |
| scope: SettingScope; |
| } | null { |
| const extensionLoader = |
| context.services.agentContext?.config.getExtensionLoader(); |
| if (!(extensionLoader instanceof ExtensionManager)) { |
| debugLogger.error( |
| `Cannot ${context.invocation?.name} extensions in this environment`, |
| ); |
| return null; |
| } |
| const parts = argumentsString.split(' '); |
| const name = parts[0]; |
| if ( |
| name === '' || |
| !( |
| (parts.length === 2 && parts[1].startsWith('--scope=')) || |
| (parts.length === 3 && parts[1] === '--scope') |
| ) |
| ) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Usage: /extensions ${context.invocation?.name} <extension> [--scope=<user|workspace|session>]`, |
| }); |
| return null; |
| } |
| let scope: SettingScope; |
| |
| if (parts.length === 2) { |
| parts.push(...parts[1].split('=')); |
| parts.splice(1, 1); |
| } |
| switch (parts[2].toLowerCase()) { |
| case 'workspace': |
| scope = SettingScope.Workspace; |
| break; |
| case 'user': |
| scope = SettingScope.User; |
| break; |
| case 'session': |
| scope = SettingScope.Session; |
| break; |
| default: |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Unsupported scope ${parts[2]}, should be one of "user", "workspace", or "session"`, |
| }); |
| debugLogger.error(); |
| return null; |
| } |
| let names: string[] = []; |
| if (name === '--all') { |
| let extensions = extensionLoader.getExtensions(); |
| if (context.invocation?.name === 'enable') { |
| extensions = extensions.filter((ext) => !ext.isActive); |
| } |
| if (context.invocation?.name === 'disable') { |
| extensions = extensions.filter((ext) => ext.isActive); |
| } |
| names = extensions.map((ext) => ext.name); |
| } else { |
| names = [name]; |
| } |
|
|
| return { |
| extensionManager: extensionLoader, |
| names, |
| scope, |
| }; |
| } |
|
|
| async function disableAction(context: CommandContext, args: string) { |
| const enableContext = getEnableDisableContext(context, args); |
| if (!enableContext) return; |
|
|
| const { names, scope, extensionManager } = enableContext; |
| for (const name of names) { |
| await extensionManager.disableExtension(name, scope); |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Extension "${name}" disabled for the scope "${scope}"`, |
| }); |
| } |
| } |
|
|
| async function enableAction(context: CommandContext, args: string) { |
| const enableContext = getEnableDisableContext(context, args); |
| if (!enableContext) return; |
|
|
| const { names, scope, extensionManager } = enableContext; |
| for (const name of names) { |
| await extensionManager.enableExtension(name, scope); |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Extension "${name}" enabled for the scope "${scope}"`, |
| }); |
|
|
| |
| const extension = extensionManager |
| .getExtensions() |
| .find((e) => e.name === name); |
|
|
| if (extension?.mcpServers) { |
| const mcpEnablementManager = McpServerEnablementManager.getInstance(); |
| const mcpClientManager = |
| context.services.agentContext?.config.getMcpClientManager(); |
| const enabledServers = await mcpEnablementManager.autoEnableServers( |
| Object.keys(extension.mcpServers ?? {}), |
| ); |
|
|
| if (mcpClientManager && enabledServers.length > 0) { |
| const restartPromises = enabledServers.map((serverName) => |
| mcpClientManager.restartServer(serverName).catch((error) => { |
| context.ui.addItem({ |
| type: MessageType.WARNING, |
| text: `Failed to restart MCP server '${serverName}': ${getErrorMessage(error)}`, |
| }); |
| }), |
| ); |
| await Promise.all(restartPromises); |
| } |
|
|
| if (enabledServers.length > 0) { |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Re-enabled MCP servers: ${enabledServers.join(', ')}`, |
| }); |
| } |
| } |
| } |
| } |
|
|
| async function installAction( |
| context: CommandContext, |
| args: string, |
| requestConsentOverride?: (consent: string) => Promise<boolean>, |
| ) { |
| const extensionLoader = |
| context.services.agentContext?.config.getExtensionLoader(); |
| if (!(extensionLoader instanceof ExtensionManager)) { |
| debugLogger.error( |
| `Cannot ${context.invocation?.name} extensions in this environment`, |
| ); |
| return; |
| } |
|
|
| const source = args.trim(); |
| if (!source) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Usage: /extensions install <source>`, |
| }); |
| return; |
| } |
|
|
| |
| let isValid = false; |
| try { |
| |
| new URL(source); |
| isValid = true; |
| } catch { |
| |
| |
| if (!/[;&|`'"]/.test(source)) { |
| isValid = true; |
| } |
| } |
|
|
| if (!isValid) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Invalid source: ${source}`, |
| }); |
| return; |
| } |
|
|
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Installing extension from "${source}"...`, |
| }); |
|
|
| try { |
| const installMetadata = await inferInstallMetadata(source); |
| const extension = await extensionLoader.installOrUpdateExtension( |
| installMetadata, |
| undefined, |
| requestConsentOverride, |
| ); |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Extension "${extension.name}" installed successfully.`, |
| }); |
| } catch (error) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Failed to install extension from "${source}": ${getErrorMessage( |
| error, |
| )}`, |
| }); |
| } |
| } |
|
|
| async function linkAction( |
| context: CommandContext, |
| args: string, |
| requestConsentOverride?: (consent: string) => Promise<boolean>, |
| ) { |
| const extensionLoader = |
| context.services.agentContext?.config.getExtensionLoader(); |
| if (!(extensionLoader instanceof ExtensionManager)) { |
| debugLogger.error( |
| `Cannot ${context.invocation?.name} extensions in this environment`, |
| ); |
| return; |
| } |
|
|
| const sourceFilepath = args.trim(); |
| if (!sourceFilepath) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Usage: /extensions link <source>`, |
| }); |
| return; |
| } |
| if (/[;&|`'"]/.test(sourceFilepath)) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Source file path contains disallowed characters: ${sourceFilepath}`, |
| }); |
| return; |
| } |
|
|
| try { |
| await stat(sourceFilepath); |
| } catch (error) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Invalid source: ${sourceFilepath}`, |
| }); |
| debugLogger.error( |
| `Failed to stat path "${sourceFilepath}": ${getErrorMessage(error)}`, |
| ); |
| return; |
| } |
|
|
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Linking extension from "${sourceFilepath}"...`, |
| }); |
|
|
| try { |
| const installMetadata: ExtensionInstallMetadata = { |
| source: sourceFilepath, |
| type: 'link', |
| }; |
| const extension = await extensionLoader.installOrUpdateExtension( |
| installMetadata, |
| undefined, |
| requestConsentOverride, |
| ); |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Extension "${extension.name}" linked successfully.`, |
| }); |
| } catch (error) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Failed to link extension from "${sourceFilepath}": ${getErrorMessage( |
| error, |
| )}`, |
| }); |
| } |
| } |
|
|
| async function uninstallAction(context: CommandContext, args: string) { |
| const extensionLoader = |
| context.services.agentContext?.config.getExtensionLoader(); |
| if (!(extensionLoader instanceof ExtensionManager)) { |
| debugLogger.error( |
| `Cannot ${context.invocation?.name} extensions in this environment`, |
| ); |
| return; |
| } |
|
|
| const uninstallArgs = args.split(' ').filter((value) => value.length > 0); |
| const all = uninstallArgs.includes('--all'); |
| const names = uninstallArgs.filter((a) => !a.startsWith('--')); |
|
|
| if (!all && names.length === 0) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Usage: /extensions uninstall <extension-names...>|--all`, |
| }); |
| return; |
| } |
|
|
| let namesToUninstall: string[] = []; |
| if (all) { |
| namesToUninstall = extensionLoader.getExtensions().map((ext) => ext.name); |
| } else { |
| namesToUninstall = names; |
| } |
|
|
| if (namesToUninstall.length === 0) { |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: all ? 'No extensions installed.' : 'No extension name provided.', |
| }); |
| return; |
| } |
|
|
| for (const extensionName of namesToUninstall) { |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Uninstalling extension "${extensionName}"...`, |
| }); |
|
|
| try { |
| await extensionLoader.uninstallExtension(extensionName, false); |
| context.ui.addItem({ |
| type: MessageType.INFO, |
| text: `Extension "${extensionName}" uninstalled successfully.`, |
| }); |
| } catch (error) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: `Failed to uninstall extension "${extensionName}": ${getErrorMessage( |
| error, |
| )}`, |
| }); |
| } |
| } |
| } |
|
|
| async function configAction(context: CommandContext, args: string) { |
| const parts = args.trim().split(/\s+/).filter(Boolean); |
| let scope = ExtensionSettingScope.USER; |
|
|
| const scopeEqIndex = parts.findIndex((p) => p.startsWith('--scope=')); |
| if (scopeEqIndex > -1) { |
| const scopeVal = parts[scopeEqIndex].split('=')[1]; |
| if (scopeVal === 'workspace') { |
| scope = ExtensionSettingScope.WORKSPACE; |
| } else if (scopeVal === 'user') { |
| scope = ExtensionSettingScope.USER; |
| } |
| parts.splice(scopeEqIndex, 1); |
| } else { |
| const scopeIndex = parts.indexOf('--scope'); |
| if (scopeIndex > -1) { |
| const scopeVal = parts[scopeIndex + 1]; |
| if (scopeVal === 'workspace' || scopeVal === 'user') { |
| scope = |
| scopeVal === 'workspace' |
| ? ExtensionSettingScope.WORKSPACE |
| : ExtensionSettingScope.USER; |
| parts.splice(scopeIndex, 2); |
| } |
| } |
| } |
|
|
| const otherArgs = parts; |
| const name = otherArgs[0]; |
| const setting = otherArgs[1]; |
|
|
| if (name) { |
| if (name.includes('/') || name.includes('\\') || name.includes('..')) { |
| context.ui.addItem({ |
| type: MessageType.ERROR, |
| text: 'Invalid extension name. Names cannot contain path separators or "..".', |
| }); |
| return; |
| } |
| } |
|
|
| const extensionManager = |
| context.services.agentContext?.config.getExtensionLoader(); |
| if (!(extensionManager instanceof ExtensionManager)) { |
| debugLogger.error( |
| `Cannot ${context.invocation?.name} extensions in this environment`, |
| ); |
| return; |
| } |
|
|
| const logger: ConfigLogger = { |
| log: (message: string) => { |
| context.ui.addItem({ type: MessageType.INFO, text: message.trim() }); |
| }, |
| error: (message: string) => |
| context.ui.addItem({ type: MessageType.ERROR, text: message }), |
| }; |
|
|
| return { |
| type: 'custom_dialog' as const, |
| component: React.createElement(ConfigExtensionDialog, { |
| extensionManager, |
| onClose: () => context.ui.removeComponent(), |
| extensionName: name, |
| settingKey: setting, |
| scope, |
| configureAll: !name && !setting, |
| loggerAdapter: logger, |
| }), |
| }; |
| } |
|
|
| |
| |
| |
| export function completeExtensions( |
| context: CommandContext, |
| partialArg: string, |
| ) { |
| let extensions = context.services.agentContext?.config.getExtensions() ?? []; |
|
|
| if (context.invocation?.name === 'enable') { |
| extensions = extensions.filter((ext) => !ext.isActive); |
| } |
| if ( |
| context.invocation?.name === 'disable' || |
| context.invocation?.name === 'restart' || |
| context.invocation?.name === 'reload' |
| ) { |
| extensions = extensions.filter((ext) => ext.isActive); |
| } |
| const extensionNames = extensions.map((ext) => ext.name); |
| const suggestions = extensionNames.filter((name) => |
| name.startsWith(partialArg), |
| ); |
|
|
| if ('--all'.startsWith(partialArg) || 'all'.startsWith(partialArg)) { |
| suggestions.unshift('--all'); |
| } |
|
|
| return suggestions; |
| } |
|
|
| export function completeExtensionsAndScopes( |
| context: CommandContext, |
| partialArg: string, |
| ) { |
| return completeExtensions(context, partialArg).flatMap((s) => [ |
| `${s} --scope user`, |
| `${s} --scope workspace`, |
| `${s} --scope session`, |
| ]); |
| } |
|
|
| const listExtensionsCommand: SlashCommand = { |
| name: 'list', |
| description: 'List active extensions', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: true, |
| takesArgs: false, |
| action: listAction, |
| }; |
|
|
| const updateExtensionsCommand: SlashCommand = { |
| name: 'update', |
| description: 'Update extensions. Usage: update <extension-names>|--all', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: false, |
| action: updateAction, |
| completion: completeExtensions, |
| }; |
|
|
| const disableCommand: SlashCommand = { |
| name: 'disable', |
| description: 'Disable an extension', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: false, |
| action: disableAction, |
| completion: completeExtensionsAndScopes, |
| }; |
|
|
| const enableCommand: SlashCommand = { |
| name: 'enable', |
| description: 'Enable an extension', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: false, |
| action: enableAction, |
| completion: completeExtensionsAndScopes, |
| }; |
|
|
| const installCommand: SlashCommand = { |
| name: 'install', |
| description: 'Install an extension from a git repo or local path', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: false, |
| action: installAction, |
| }; |
|
|
| const linkCommand: SlashCommand = { |
| name: 'link', |
| description: 'Link an extension from a local path', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: false, |
| action: linkAction, |
| }; |
|
|
| const uninstallCommand: SlashCommand = { |
| name: 'uninstall', |
| altNames: ['delete'], |
| description: 'Uninstall an extension', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: false, |
| action: uninstallAction, |
| completion: completeExtensions, |
| }; |
|
|
| const exploreExtensionsCommand: SlashCommand = { |
| name: 'explore', |
| description: 'Open extensions page in your browser', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: true, |
| takesArgs: false, |
| action: exploreAction, |
| }; |
|
|
| const reloadCommand: SlashCommand = { |
| name: 'reload', |
| altNames: ['restart'], |
| description: 'Reload all extensions', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: false, |
| action: restartAction, |
| completion: completeExtensions, |
| }; |
|
|
| const configCommand: SlashCommand = { |
| name: 'config', |
| description: 'Configure extension settings', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: false, |
| action: configAction, |
| }; |
|
|
| import { parseSlashCommand } from '../../utils/commands.js'; |
|
|
| export function extensionsCommand( |
| enableExtensionReloading?: boolean, |
| ): SlashCommand { |
| const conditionalCommands = enableExtensionReloading |
| ? [ |
| disableCommand, |
| enableCommand, |
| installCommand, |
| uninstallCommand, |
| linkCommand, |
| configCommand, |
| ] |
| : []; |
| const subCommands = [ |
| listExtensionsCommand, |
| updateExtensionsCommand, |
| exploreExtensionsCommand, |
| reloadCommand, |
| ...conditionalCommands, |
| ]; |
|
|
| return { |
| name: 'extensions', |
| description: 'Manage extensions', |
| kind: CommandKind.BUILT_IN, |
| autoExecute: false, |
| subCommands, |
| action: async (context, args) => { |
| if (args) { |
| const parsed = parseSlashCommand(`/${args}`, subCommands); |
| if (parsed.commandToExecute?.action) { |
| return parsed.commandToExecute.action(context, parsed.args); |
| } |
| } |
| |
| return listExtensionsCommand.action!(context, args); |
| }, |
| }; |
| } |
|
|