Add flag to update all extensions (#7321)

This commit is contained in:
christine betts
2025-08-29 17:24:17 +00:00
committed by GitHub
parent 6a9fb6d2ea
commit af6a792caa
3 changed files with 73 additions and 32 deletions
+40 -11
View File
@@ -5,43 +5,72 @@
*/ */
import type { CommandModule } from 'yargs'; import type { CommandModule } from 'yargs';
import { updateExtension } from '../../config/extension.js'; import {
updateExtensionByName,
updateAllUpdatableExtensions,
type ExtensionUpdateInfo,
} from '../../config/extension.js';
import { getErrorMessage } from '../../utils/errors.js'; import { getErrorMessage } from '../../utils/errors.js';
interface UpdateArgs { interface UpdateArgs {
name: string; name?: string;
all?: boolean;
} }
const updateOutput = (info: ExtensionUpdateInfo) =>
`Extension "${info.name}" successfully updated: ${info.originalVersion}${info.updatedVersion}.`;
export async function handleUpdate(args: UpdateArgs) { export async function handleUpdate(args: UpdateArgs) {
if (args.all) {
try { try {
// TODO(chrstnb): we should list extensions if the requested extension is not installed. const updateInfos = await updateAllUpdatableExtensions();
const updatedExtensionInfo = await updateExtension(args.name); if (updateInfos.length === 0) {
if (!updatedExtensionInfo) { console.log('No extensions to update.');
console.log(`Extension "${args.name}" failed to update.`);
return; return;
} }
console.log(updateInfos.map((info) => updateOutput(info)).join('\n'));
} catch (error) {
console.error(getErrorMessage(error));
}
return;
}
if (args.name)
try {
// TODO(chrstnb): we should list extensions if the requested extension is not installed.
const updatedExtensionInfo = await updateExtensionByName(args.name);
console.log( console.log(
`Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion}${updatedExtensionInfo.updatedVersion}.`, `Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion}${updatedExtensionInfo.updatedVersion}.`,
); );
} catch (error) { } catch (error) {
console.error(getErrorMessage(error)); console.error(getErrorMessage(error));
process.exit(1);
} }
} }
export const updateCommand: CommandModule = { export const updateCommand: CommandModule = {
command: 'update <name>', command: 'update [--all] [name]',
describe: 'Updates an extension.', describe:
'Updates all extensions or a named extension to the latest version.',
builder: (yargs) => builder: (yargs) =>
yargs yargs
.positional('name', { .positional('name', {
describe: 'The name of the extension to update.', describe: 'The name of the extension to update.',
type: 'string', type: 'string',
}) })
.check((_argv) => true), .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) => { handler: async (argv) => {
await handleUpdate({ await handleUpdate({
name: argv['name'] as string, name: argv['name'] as string | undefined,
all: argv['all'] as boolean | undefined,
}); });
}, },
}; };
+2 -8
View File
@@ -635,13 +635,11 @@ describe('updateExtension', () => {
}); });
it('should update a git-installed extension', async () => { it('should update a git-installed extension', async () => {
// 1. "Install" an extension
const gitUrl = 'https://github.com/google/gemini-extensions.git'; const gitUrl = 'https://github.com/google/gemini-extensions.git';
const extensionName = 'gemini-extensions'; const extensionName = 'gemini-extensions';
const targetExtDir = path.join(userExtensionsDir, extensionName); const targetExtDir = path.join(userExtensionsDir, extensionName);
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME); const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
// Create the "installed" extension directory and files
fs.mkdirSync(targetExtDir, { recursive: true }); fs.mkdirSync(targetExtDir, { recursive: true });
fs.writeFileSync( fs.writeFileSync(
path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME), path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME),
@@ -652,10 +650,8 @@ describe('updateExtension', () => {
JSON.stringify({ source: gitUrl, type: 'git' }), JSON.stringify({ source: gitUrl, type: 'git' }),
); );
// 2. Mock the git clone for the update
const clone = vi.fn().mockImplementation(async (_, destination) => { const clone = vi.fn().mockImplementation(async (_, destination) => {
fs.mkdirSync(destination, { recursive: true }); fs.mkdirSync(destination, { recursive: true });
// This is the "updated" version
fs.writeFileSync( fs.writeFileSync(
path.join(destination, EXTENSIONS_CONFIG_FILENAME), path.join(destination, EXTENSIONS_CONFIG_FILENAME),
JSON.stringify({ name: extensionName, version: '1.1.0' }), JSON.stringify({ name: extensionName, version: '1.1.0' }),
@@ -667,16 +663,14 @@ describe('updateExtension', () => {
clone, clone,
} as unknown as SimpleGit); } as unknown as SimpleGit);
// 3. Call updateExtension const updateInfo = await updateExtension(loadExtension(targetExtDir));
const updateInfo = await updateExtension(extensionName);
// 4. Assertions
expect(updateInfo).toEqual({ expect(updateInfo).toEqual({
name: 'gemini-extensions',
originalVersion: '1.0.0', originalVersion: '1.0.0',
updatedVersion: '1.1.0', updatedVersion: '1.1.0',
}); });
// Check that the config file reflects the new version
const updatedConfig = JSON.parse( const updatedConfig = JSON.parse(
fs.readFileSync( fs.readFileSync(
path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME), path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME),
+24 -6
View File
@@ -44,6 +44,7 @@ export interface ExtensionInstallMetadata {
} }
export interface ExtensionUpdateInfo { export interface ExtensionUpdateInfo {
name: string;
originalVersion: string; originalVersion: string;
updatedVersion: string; updatedVersion: string;
} }
@@ -444,10 +445,10 @@ export function toOutputString(extension: Extension): string {
return output; return output;
} }
export async function updateExtension( export async function updateExtensionByName(
extensionName: string, extensionName: string,
cwd: string = process.cwd(), cwd: string = process.cwd(),
): Promise<ExtensionUpdateInfo | undefined> { ): Promise<ExtensionUpdateInfo> {
const installedExtensions = loadUserExtensions(); const installedExtensions = loadUserExtensions();
const extension = installedExtensions.find( const extension = installedExtensions.find(
(installed) => installed.config.name === extensionName, (installed) => installed.config.name === extensionName,
@@ -457,16 +458,21 @@ export async function updateExtension(
`Extension "${extensionName}" not found. Run gemini extensions list to see available extensions.`, `Extension "${extensionName}" not found. Run gemini extensions list to see available extensions.`,
); );
} }
return await updateExtension(extension, cwd);
}
export async function updateExtension(
extension: Extension,
cwd: string = process.cwd(),
): Promise<ExtensionUpdateInfo> {
if (!extension.installMetadata) { if (!extension.installMetadata) {
throw new Error( throw new Error(`Extension ${extension.config.name} cannot be updated.`);
`Extension cannot be updated because it is missing the .gemini-extension.install.json file. To update manually, uninstall and then reinstall the updated version.`,
);
} }
const originalVersion = extension.config.version; const originalVersion = extension.config.version;
const tempDir = await ExtensionStorage.createTmpDir(); const tempDir = await ExtensionStorage.createTmpDir();
try { try {
await copyExtension(extension.path, tempDir); await copyExtension(extension.path, tempDir);
await uninstallExtension(extensionName, cwd); await uninstallExtension(extension.config.name, cwd);
await installExtension(extension.installMetadata, cwd); await installExtension(extension.installMetadata, cwd);
const updatedExtension = loadExtension(extension.path); const updatedExtension = loadExtension(extension.path);
@@ -475,6 +481,7 @@ export async function updateExtension(
} }
const updatedVersion = updatedExtension.config.version; const updatedVersion = updatedExtension.config.version;
return { return {
name: extension.config.name,
originalVersion, originalVersion,
updatedVersion, updatedVersion,
}; };
@@ -537,3 +544,14 @@ function removeFromDisabledExtensions(
settings.setValue(scope, 'extensions', extensionSettings); settings.setValue(scope, 'extensions', extensionSettings);
} }
} }
export async function updateAllUpdatableExtensions(
cwd: string = process.cwd(),
): Promise<ExtensionUpdateInfo[]> {
const extensions = loadExtensions(cwd).filter(
(extension) => !!extension.installMetadata,
);
return await Promise.all(
extensions.map((extension) => updateExtension(extension, cwd)),
);
}