/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type { CommandModule } from 'yargs'; import { loadExtensions, annotateActiveExtensions, ExtensionStorage, requestConsentNonInteractive, } from '../../config/extension.js'; import { exitCli } from '../utils.js'; import { updateAllUpdatableExtensions, type ExtensionUpdateInfo, checkForAllExtensionUpdates, updateExtension, } from '../../config/extensions/update.js'; import { checkForExtensionUpdate } from '../../config/extensions/github.js'; import { getErrorMessage } from '../../utils/errors.js'; import { type ExtensionUpdateAction, type ExtensionUpdateStatus, ExtensionUpdateState, } from '../../ui/state/extensions.js'; import { ExtensionEnablementManager } from '../../config/extensions/extensionEnablement.js'; interface UpdateArgs { name?: string; all?: boolean; } const updateOutput = (info: ExtensionUpdateInfo) => `Extension "${info.name}" successfully updated: ${info.originalVersion} → ${info.updatedVersion}.`; type LoadedExtension = ReturnType[number]; type ExtensionStateMap = Map; function loadUpdateExtensions(args: UpdateArgs, workingDir: string) { const extensionEnablementManager = new ExtensionEnablementManager( ExtensionStorage.getUserExtensionsDir(), // Force enable named extensions, otherwise we will only update the enabled // ones. args.name ? [args.name] : [], ); const allExtensions = loadExtensions(extensionEnablementManager); return annotateActiveExtensions( allExtensions, workingDir, extensionEnablementManager, ); } function findNamedExtension( extensions: LoadedExtension[], name: string, ): LoadedExtension | undefined { const extension = extensions.find((extension) => extension.name === name); if (!extension) { globalThis.console.log(`Extension "${name}" not found.`); return undefined; } if (!extension.installMetadata) { globalThis.console.log( `Unable to install extension "${name}" due to missing install metadata`, ); return undefined; } return extension; } async function updateNamedExtension( name: string, extensions: LoadedExtension[], workingDir: string, ) { try { const extension = findNamedExtension(extensions, name); if (!extension) return; let updateState = ExtensionUpdateState.UNKNOWN as ExtensionUpdateState; await checkForExtensionUpdate(extension, (state) => { updateState = state; }); if (updateState !== ExtensionUpdateState.UPDATE_AVAILABLE) { globalThis.console.log(`Extension "${name}" is already up to date.`); return; } const updatedExtensionInfo = (await updateExtension( extension, workingDir, requestConsentNonInteractive, updateState, (action) => { if (action.type === 'SET_STATE') { updateState = action.payload.state; } }, undefined, // enableExtensionReloading - undefined means use default behavior ))!; if ( updatedExtensionInfo.originalVersion !== updatedExtensionInfo.updatedVersion ) { globalThis.console.log(updateOutput({ ...updatedExtensionInfo, name })); } else { globalThis.console.log(`Extension "${name}" is already up to date.`); } } catch (error) { globalThis.console.error(getErrorMessage(error)); } } function recordExtensionState(extensionState: ExtensionStateMap) { return (action: ExtensionUpdateAction) => { if (action.type === 'SET_STATE') { extensionState.set(action.payload.name, { status: action.payload.state, notified: false, }); } }; } async function updateAllExtensions( extensions: LoadedExtension[], workingDir: string, ) { try { const extensionState: ExtensionStateMap = new Map(); const dispatch = recordExtensionState(extensionState); await checkForAllExtensionUpdates(extensions, dispatch, workingDir); const updateInfos = ( await updateAllUpdatableExtensions( workingDir, requestConsentNonInteractive, extensions, extensionState, dispatch, ) ).filter((info) => info.originalVersion !== info.updatedVersion); if (updateInfos.length === 0) { globalThis.console.log('No extensions to update.'); return; } globalThis.console.log( updateInfos.map((info) => updateOutput(info)).join('\n'), ); } catch (error) { globalThis.console.error(getErrorMessage(error)); } } export async function handleUpdate(args: UpdateArgs) { const workingDir = process.cwd(); const extensions = loadUpdateExtensions(args, workingDir); if (args.name) { await updateNamedExtension(args.name, extensions, workingDir); } if (args.all === true) { await updateAllExtensions(extensions, workingDir); } } export const updateCommand: CommandModule = { command: 'update [] [--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) => { // argv.all is boolean | undefined, argv.name is string | undefined if ( argv.all !== true && (argv.name === undefined || argv.name === '') ) { throw new Error('Either an extension name or --all must be provided'); } return true; }), handler: async (argv) => { await handleUpdate({ name: argv['name'] as string | undefined, all: argv['all'] as boolean | undefined, }); await exitCli(); }, };