Files
gemini-cli/packages/cli/src/commands/extensions/update.ts

169 lines
5.1 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import {
updateAllUpdatableExtensions,
type ExtensionUpdateInfo,
checkForAllExtensionUpdates,
updateExtension,
} from '../../config/extensions/update.js';
import { checkForExtensionUpdate } from '../../config/extensions/github.js';
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
import {
coreEvents,
debugLogger,
getErrorMessage,
} from '@google/gemini-cli-core';
import { ExtensionManager } from '../../config/extension-manager.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { loadSettings } from '../../config/settings.js';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
import { exitCli } from '../utils.js';
interface UpdateArgs {
name?: string;
all?: boolean;
}
const updateOutput = (info: ExtensionUpdateInfo) =>
`Extension "${info.name}" successfully updated: ${info.originalVersion}${info.updatedVersion}.`;
export async function handleUpdate(args: UpdateArgs) {
const workspaceDir = process.cwd();
const settings = loadSettings(workspaceDir).merged;
const extensionManager = new ExtensionManager({
workspaceDir,
requestConsent: requestConsentNonInteractive,
requestSetting: promptForSetting,
settings,
});
const extensions = await extensionManager.loadExtensions();
if (args.name) {
try {
const extension = extensions.find(
(extension) => extension.name === args.name,
);
if (!extension) {
if (extensions.length === 0) {
coreEvents.emitFeedback(
'error',
`Extension "${args.name}" not found.\n\nNo extensions installed.`,
);
return;
}
const installedExtensions = extensions
.map((extension) => `${extension.name} (${extension.version})`)
.join('\n');
coreEvents.emitFeedback(
'error',
`Extension "${args.name}" not found.\n\nInstalled extensions:\n${installedExtensions}\n\nRun "gemini extensions list" for details.`,
);
return;
}
if (!extension.installMetadata) {
debugLogger.log(
`Unable to install extension "${args.name}" due to missing install metadata`,
);
return;
}
const updateState = await checkForExtensionUpdate(
extension,
extensionManager,
);
if (updateState !== ExtensionUpdateState.UPDATE_AVAILABLE) {
debugLogger.log(`Extension "${args.name}" is already up to date.`);
return;
}
const updatedExtensionInfo = (await updateExtension(
extension,
extensionManager,
updateState,
() => {},
settings.experimental?.extensionReloading,
))!;
if (
updatedExtensionInfo.originalVersion !==
updatedExtensionInfo.updatedVersion
) {
debugLogger.log(
`Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion}${updatedExtensionInfo.updatedVersion}.`,
);
} else {
debugLogger.log(`Extension "${args.name}" is already up to date.`);
}
} catch (error) {
debugLogger.error(getErrorMessage(error));
}
}
if (args.all) {
try {
const extensionState = new Map();
await checkForAllExtensionUpdates(
extensions,
extensionManager,
(action) => {
if (action.type === 'SET_STATE') {
extensionState.set(action.payload.name, {
status: action.payload.state,
});
}
},
);
let updateInfos = await updateAllUpdatableExtensions(
extensions,
extensionState,
extensionManager,
() => {},
);
updateInfos = updateInfos.filter(
(info) => info.originalVersion !== info.updatedVersion,
);
if (updateInfos.length === 0) {
debugLogger.log('No extensions to update.');
return;
}
debugLogger.log(updateInfos.map((info) => updateOutput(info)).join('\n'));
} catch (error) {
debugLogger.error(getErrorMessage(error));
}
}
}
export const updateCommand: CommandModule = {
command: 'update [<name>] [--all]',
describe:
'Updates all extensions or a named extension to the latest version.',
builder: (yargs) =>
yargs
.positional('name', {
describe: 'The name of the extension to update.',
type: 'string',
})
.option('all', {
describe: 'Update all extensions.',
type: 'boolean',
})
.conflicts('name', 'all')
.check((argv) => {
if (!argv.all && !argv.name) {
throw new Error('Either an extension name or --all must be provided');
}
return true;
}),
handler: async (argv) => {
await handleUpdate({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
name: argv['name'] as string | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
all: argv['all'] as boolean | undefined,
});
await exitCli();
},
};