2025-09-18 14:49:47 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
|
|
|
|
import { getErrorMessage } from '../../utils/errors.js';
|
|
|
|
|
import { ExtensionUpdateState } from '../state/extensions.js';
|
2025-09-29 14:19:19 -07:00
|
|
|
import { useCallback, useState } from 'react';
|
2025-09-18 14:49:47 -07:00
|
|
|
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
2025-09-29 14:19:19 -07:00
|
|
|
import { MessageType, type ConfirmationRequest } from '../types.js';
|
2025-09-18 14:49:47 -07:00
|
|
|
import {
|
|
|
|
|
checkForAllExtensionUpdates,
|
|
|
|
|
updateExtension,
|
|
|
|
|
} from '../../config/extensions/update.js';
|
2025-09-25 10:57:59 -07:00
|
|
|
import { requestConsentInteractive } from '../../config/extension.js';
|
2025-09-18 14:49:47 -07:00
|
|
|
|
|
|
|
|
export const useExtensionUpdates = (
|
|
|
|
|
extensions: GeminiCLIExtension[],
|
|
|
|
|
addItem: UseHistoryManagerReturn['addItem'],
|
|
|
|
|
cwd: string,
|
|
|
|
|
) => {
|
|
|
|
|
const [extensionsUpdateState, setExtensionsUpdateState] = useState(
|
|
|
|
|
new Map<string, ExtensionUpdateState>(),
|
|
|
|
|
);
|
2025-09-25 10:58:43 -07:00
|
|
|
const [isChecking, setIsChecking] = useState(false);
|
2025-09-29 14:19:19 -07:00
|
|
|
const [confirmUpdateExtensionRequests, setConfirmUpdateExtensionRequests] =
|
|
|
|
|
useState<
|
|
|
|
|
Array<{
|
|
|
|
|
prompt: React.ReactNode;
|
|
|
|
|
onConfirm: (confirmed: boolean) => void;
|
|
|
|
|
}>
|
|
|
|
|
>([]);
|
|
|
|
|
const addConfirmUpdateExtensionRequest = useCallback(
|
|
|
|
|
(original: ConfirmationRequest) => {
|
|
|
|
|
const wrappedRequest = {
|
|
|
|
|
prompt: original.prompt,
|
|
|
|
|
onConfirm: (confirmed: boolean) => {
|
|
|
|
|
// Remove it from the outstanding list of requests by identity.
|
|
|
|
|
setConfirmUpdateExtensionRequests((prev) =>
|
|
|
|
|
prev.filter((r) => r !== wrappedRequest),
|
|
|
|
|
);
|
|
|
|
|
original.onConfirm(confirmed);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
setConfirmUpdateExtensionRequests((prev) => [...prev, wrappedRequest]);
|
|
|
|
|
},
|
|
|
|
|
[setConfirmUpdateExtensionRequests],
|
|
|
|
|
);
|
2025-09-25 10:58:43 -07:00
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
|
if (isChecking) return;
|
|
|
|
|
setIsChecking(true);
|
|
|
|
|
try {
|
2025-09-18 14:49:47 -07:00
|
|
|
const updateState = await checkForAllExtensionUpdates(
|
|
|
|
|
extensions,
|
|
|
|
|
extensionsUpdateState,
|
|
|
|
|
setExtensionsUpdateState,
|
|
|
|
|
);
|
2025-09-25 10:58:43 -07:00
|
|
|
let extensionsWithUpdatesCount = 0;
|
2025-09-18 14:49:47 -07:00
|
|
|
for (const extension of extensions) {
|
|
|
|
|
const prevState = extensionsUpdateState.get(extension.name);
|
|
|
|
|
const currentState = updateState.get(extension.name);
|
|
|
|
|
if (
|
|
|
|
|
prevState === currentState ||
|
|
|
|
|
currentState !== ExtensionUpdateState.UPDATE_AVAILABLE
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (extension.installMetadata?.autoUpdate) {
|
2025-09-25 10:57:59 -07:00
|
|
|
updateExtension(
|
|
|
|
|
extension,
|
|
|
|
|
cwd,
|
2025-09-29 14:19:19 -07:00
|
|
|
(description) =>
|
|
|
|
|
requestConsentInteractive(
|
|
|
|
|
description,
|
|
|
|
|
addConfirmUpdateExtensionRequest,
|
|
|
|
|
),
|
2025-09-25 10:57:59 -07:00
|
|
|
currentState,
|
|
|
|
|
(newState) => {
|
|
|
|
|
setExtensionsUpdateState((prev) => {
|
|
|
|
|
const finalState = new Map(prev);
|
|
|
|
|
finalState.set(extension.name, newState);
|
|
|
|
|
return finalState;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-09-18 14:49:47 -07:00
|
|
|
.then((result) => {
|
|
|
|
|
if (!result) return;
|
|
|
|
|
addItem(
|
|
|
|
|
{
|
|
|
|
|
type: MessageType.INFO,
|
|
|
|
|
text: `Extension "${extension.name}" successfully updated: ${result.originalVersion} → ${result.updatedVersion}.`,
|
|
|
|
|
},
|
|
|
|
|
Date.now(),
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
2025-09-29 14:19:19 -07:00
|
|
|
addItem(
|
|
|
|
|
{
|
|
|
|
|
type: MessageType.ERROR,
|
|
|
|
|
text: getErrorMessage(error),
|
|
|
|
|
},
|
|
|
|
|
Date.now(),
|
2025-09-18 14:49:47 -07:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2025-09-25 10:58:43 -07:00
|
|
|
extensionsWithUpdatesCount++;
|
2025-09-18 14:49:47 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-25 10:58:43 -07:00
|
|
|
if (extensionsWithUpdatesCount > 0) {
|
|
|
|
|
const s = extensionsWithUpdatesCount > 1 ? 's' : '';
|
|
|
|
|
addItem(
|
|
|
|
|
{
|
|
|
|
|
type: MessageType.INFO,
|
|
|
|
|
text: `You have ${extensionsWithUpdatesCount} extension${s} with an update available, run "/extensions list" for more information.`,
|
|
|
|
|
},
|
|
|
|
|
Date.now(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setIsChecking(false);
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
2025-09-18 14:49:47 -07:00
|
|
|
return {
|
|
|
|
|
extensionsUpdateState,
|
|
|
|
|
setExtensionsUpdateState,
|
2025-09-29 14:19:19 -07:00
|
|
|
confirmUpdateExtensionRequests,
|
|
|
|
|
addConfirmUpdateExtensionRequest,
|
2025-09-18 14:49:47 -07:00
|
|
|
};
|
|
|
|
|
};
|