mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-15 16:41:11 -07:00
Make extensions install work
This commit is contained in:
@@ -153,6 +153,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
async installOrUpdateExtension(
|
||||
installMetadata: ExtensionInstallMetadata,
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
requestConsentOverride?: (consent: string) => Promise<boolean>,
|
||||
): Promise<GeminiCLIExtension> {
|
||||
if (
|
||||
this.settings.security?.allowedExtensions &&
|
||||
@@ -243,7 +244,7 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
(result.failureReason === 'no release data' &&
|
||||
installMetadata.type === 'git') ||
|
||||
// Otherwise ask the user if they would like to try a git clone.
|
||||
(await this.requestConsent(
|
||||
(await (requestConsentOverride ?? this.requestConsent)(
|
||||
`Error downloading github release for ${installMetadata.source} with the following error: ${result.errorMessage}.
|
||||
|
||||
Would you like to attempt to install via "git clone" instead?`,
|
||||
@@ -301,7 +302,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
await maybeRequestConsentOrFail(
|
||||
newExtensionConfig,
|
||||
this.requestConsent,
|
||||
requestConsentOverride ?? this.requestConsent,
|
||||
newHasHooks,
|
||||
previousExtensionConfig,
|
||||
previousHasHooks,
|
||||
|
||||
@@ -560,10 +560,14 @@ describe('extensionsCommand', () => {
|
||||
mockInstallExtension.mockResolvedValue({ name: packageName });
|
||||
await installAction!(mockContext, packageName);
|
||||
expect(inferInstallMetadata).toHaveBeenCalledWith(packageName);
|
||||
expect(mockInstallExtension).toHaveBeenCalledWith({
|
||||
source: packageName,
|
||||
type: 'git',
|
||||
});
|
||||
expect(mockInstallExtension).toHaveBeenCalledWith(
|
||||
{
|
||||
source: packageName,
|
||||
type: 'git',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.INFO,
|
||||
text: `Installing extension from "${packageName}"...`,
|
||||
@@ -585,10 +589,14 @@ describe('extensionsCommand', () => {
|
||||
|
||||
await installAction!(mockContext, packageName);
|
||||
expect(inferInstallMetadata).toHaveBeenCalledWith(packageName);
|
||||
expect(mockInstallExtension).toHaveBeenCalledWith({
|
||||
source: packageName,
|
||||
type: 'git',
|
||||
});
|
||||
expect(mockInstallExtension).toHaveBeenCalledWith(
|
||||
{
|
||||
source: packageName,
|
||||
type: 'git',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.ERROR,
|
||||
text: `Failed to install extension from "${packageName}": ${errorMessage}`,
|
||||
|
||||
@@ -279,8 +279,8 @@ async function exploreAction(
|
||||
return {
|
||||
type: 'custom_dialog' as const,
|
||||
component: React.createElement(ExtensionRegistryView, {
|
||||
onSelect: async (extension) => {
|
||||
await installAction(context, extension.url);
|
||||
onSelect: async (extension, requestConsentOverride) => {
|
||||
await installAction(context, extension.url, requestConsentOverride);
|
||||
},
|
||||
onClose: () => context.ui.removeComponent(),
|
||||
extensionManager,
|
||||
@@ -456,7 +456,11 @@ async function enableAction(context: CommandContext, args: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function installAction(context: CommandContext, args: string) {
|
||||
async function installAction(
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
requestConsentOverride?: (consent: string) => Promise<boolean>,
|
||||
) {
|
||||
const extensionLoader = context.services.config?.getExtensionLoader();
|
||||
if (!(extensionLoader instanceof ExtensionManager)) {
|
||||
debugLogger.error(
|
||||
@@ -503,8 +507,11 @@ async function installAction(context: CommandContext, args: string) {
|
||||
|
||||
try {
|
||||
const installMetadata = await inferInstallMetadata(source);
|
||||
const extension =
|
||||
await extensionLoader.installOrUpdateExtension(installMetadata);
|
||||
const extension = await extensionLoader.installOrUpdateExtension(
|
||||
installMetadata,
|
||||
undefined,
|
||||
requestConsentOverride,
|
||||
);
|
||||
context.ui.addItem({
|
||||
type: MessageType.INFO,
|
||||
text: `Extension "${extension.name}" installed successfully.`,
|
||||
|
||||
@@ -14,24 +14,52 @@ import { theme } from '../../semantic-colors.js';
|
||||
export interface ExtensionDetailsProps {
|
||||
extension: RegistryExtension;
|
||||
onBack: () => void;
|
||||
onInstall: () => void;
|
||||
onInstall: (
|
||||
requestConsentOverride: (consent: string) => Promise<boolean>,
|
||||
) => void;
|
||||
isInstalled: boolean;
|
||||
}
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export function ExtensionDetails({
|
||||
extension,
|
||||
onBack,
|
||||
onInstall,
|
||||
isInstalled,
|
||||
}: ExtensionDetailsProps): React.JSX.Element {
|
||||
const [consentRequest, setConsentRequest] = useState<{
|
||||
prompt: string;
|
||||
resolve: (value: boolean) => void;
|
||||
} | null>(null);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (consentRequest) {
|
||||
if (keyMatchers[Command.ESCAPE](key)) {
|
||||
consentRequest.resolve(false);
|
||||
setConsentRequest(null);
|
||||
setIsInstalling(false);
|
||||
return true;
|
||||
}
|
||||
if (keyMatchers[Command.RETURN](key)) {
|
||||
consentRequest.resolve(true);
|
||||
setConsentRequest(null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.ESCAPE](key)) {
|
||||
onBack();
|
||||
return true;
|
||||
}
|
||||
if (keyMatchers[Command.RETURN](key) && !isInstalled) {
|
||||
onInstall();
|
||||
if (keyMatchers[Command.RETURN](key) && !isInstalled && !isInstalling) {
|
||||
setIsInstalling(true);
|
||||
onInstall((prompt: string) => new Promise((resolve) => {
|
||||
setConsentRequest({ prompt, resolve });
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -39,6 +67,47 @@ export function ExtensionDetails({
|
||||
{ isActive: true, priority: true },
|
||||
);
|
||||
|
||||
if (consentRequest) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
height="100%"
|
||||
borderStyle="round"
|
||||
borderColor={theme.status.warning}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text color={theme.text.primary}>{consentRequest.prompt}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} />
|
||||
<Box flexDirection="row" justifyContent="space-between" marginTop={1}>
|
||||
<Text color={theme.text.secondary}>[Esc] Cancel</Text>
|
||||
<Text color={theme.text.primary}>[Enter] Accept</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (isInstalling) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
height="100%"
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text color={theme.text.primary}>
|
||||
Installing {extension.extensionName}...
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
@@ -83,35 +152,33 @@ export function ExtensionDetails({
|
||||
|
||||
{/* Features List */}
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
{extension.hasMCP && (
|
||||
<Box marginRight={1}>
|
||||
<Text color={theme.text.primary}>MCP </Text>
|
||||
<Text color={theme.text.secondary}>|</Text>
|
||||
</Box>
|
||||
)}
|
||||
{extension.hasContext && (
|
||||
<Box marginRight={1}>
|
||||
<Text color={theme.status.error}>Context file </Text>
|
||||
<Text color={theme.text.secondary}>|</Text>
|
||||
</Box>
|
||||
)}
|
||||
{extension.hasHooks && (
|
||||
<Box marginRight={1}>
|
||||
<Text color={theme.status.warning}>Hooks </Text>
|
||||
<Text color={theme.text.secondary}>|</Text>
|
||||
</Box>
|
||||
)}
|
||||
{extension.hasSkills && (
|
||||
<Box marginRight={1}>
|
||||
<Text color={theme.status.success}>Skills </Text>
|
||||
<Text color={theme.text.secondary}>|</Text>
|
||||
</Box>
|
||||
)}
|
||||
{extension.hasCustomCommands && (
|
||||
<Box marginRight={1}>
|
||||
<Text color={theme.text.primary}>Commands</Text>
|
||||
</Box>
|
||||
)}
|
||||
{[
|
||||
extension.hasMCP && { label: 'MCP', color: theme.text.primary },
|
||||
extension.hasContext && {
|
||||
label: 'Context file',
|
||||
color: theme.status.error,
|
||||
},
|
||||
extension.hasHooks && { label: 'Hooks', color: theme.status.warning },
|
||||
extension.hasSkills && {
|
||||
label: 'Skills',
|
||||
color: theme.status.success,
|
||||
},
|
||||
extension.hasCustomCommands && {
|
||||
label: 'Commands',
|
||||
color: theme.text.primary,
|
||||
},
|
||||
]
|
||||
.filter((f): f is { label: string; color: string } => !!f)
|
||||
.map((feature, index, array) => (
|
||||
<Box key={feature.label} flexDirection="row">
|
||||
<Text color={feature.color}>{feature.label} </Text>
|
||||
{index < array.length - 1 && (
|
||||
<Box marginRight={1}>
|
||||
<Text color={theme.text.secondary}>|</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Details about MCP / Context */}
|
||||
|
||||
@@ -227,7 +227,10 @@ describe('ExtensionRegistryView', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(mockExtensions[0]);
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
mockExtensions[0],
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,10 @@ import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { ExtensionDetails } from './ExtensionDetails.js';
|
||||
|
||||
interface ExtensionRegistryViewProps {
|
||||
onSelect?: (extension: RegistryExtension) => void | Promise<void>;
|
||||
onSelect?: (
|
||||
extension: RegistryExtension,
|
||||
requestConsentOverride?: (consent: string) => Promise<boolean>,
|
||||
) => void | Promise<void>;
|
||||
onClose?: () => void;
|
||||
extensionManager: ExtensionManager;
|
||||
}
|
||||
@@ -76,10 +79,16 @@ export function ExtensionRegistryView({
|
||||
}, []);
|
||||
|
||||
const handleInstall = useCallback(
|
||||
async (extension: RegistryExtension) => {
|
||||
await onSelect?.(extension);
|
||||
async (
|
||||
extension: RegistryExtension,
|
||||
requestConsentOverride?: (consent: string) => Promise<boolean>,
|
||||
) => {
|
||||
await onSelect?.(extension, requestConsentOverride);
|
||||
|
||||
// Refresh installed extensions list
|
||||
setInstalledExtensions(extensionManager.getExtensions());
|
||||
|
||||
// Go back to the search page (list view)
|
||||
setSelectedExtension(null);
|
||||
},
|
||||
[onSelect, extensionManager],
|
||||
@@ -246,7 +255,9 @@ export function ExtensionRegistryView({
|
||||
<ExtensionDetails
|
||||
extension={selectedExtension}
|
||||
onBack={handleBack}
|
||||
onInstall={() => handleInstall(selectedExtension)}
|
||||
onInstall={(requestConsentOverride) =>
|
||||
handleInstall(selectedExtension, requestConsentOverride)
|
||||
}
|
||||
isInstalled={installedExtensions.some(
|
||||
(e) => e.name === selectedExtension.extensionName,
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user