From d9828e257143b7020d63f15bf387c374c0449115 Mon Sep 17 00:00:00 2001 From: christine betts Date: Sun, 21 Sep 2025 23:44:58 -0400 Subject: [PATCH] Reinstate support for updating locally-installed extensions (#8833) --- .../cli/src/commands/extensions/install.ts | 1 - .../cli/src/commands/extensions/update.ts | 46 +++++++-------- .../cli/src/config/extensions/github.test.ts | 2 +- packages/cli/src/config/extensions/github.ts | 21 +++++++ .../cli/src/config/extensions/update.test.ts | 59 +++++++++++++++++-- packages/cli/src/config/extensions/update.ts | 19 +++--- 6 files changed, 111 insertions(+), 37 deletions(-) diff --git a/packages/cli/src/commands/extensions/install.ts b/packages/cli/src/commands/extensions/install.ts index 44fad4dbba..0f289bb08c 100644 --- a/packages/cli/src/commands/extensions/install.ts +++ b/packages/cli/src/commands/extensions/install.ts @@ -20,7 +20,6 @@ interface InstallArgs { export async function handleInstall(args: InstallArgs) { try { let installMetadata: ExtensionInstallMetadata; - if (args.source) { const { source } = args; if ( diff --git a/packages/cli/src/commands/extensions/update.ts b/packages/cli/src/commands/extensions/update.ts index 1cda6c36f3..7a39f78add 100644 --- a/packages/cli/src/commands/extensions/update.ts +++ b/packages/cli/src/commands/extensions/update.ts @@ -35,28 +35,7 @@ export async function handleUpdate(args: UpdateArgs) { allExtensions.map((e) => e.config.name), workingDir, ); - - if (args.all) { - try { - let updateInfos = await updateAllUpdatableExtensions( - workingDir, - extensions, - await checkForAllExtensionUpdates(extensions, new Map(), (_) => {}), - () => {}, - ); - updateInfos = updateInfos.filter( - (info) => info.originalVersion !== info.updatedVersion, - ); - if (updateInfos.length === 0) { - console.log('No extensions to update.'); - return; - } - console.log(updateInfos.map((info) => updateOutput(info)).join('\n')); - } catch (error) { - console.error(getErrorMessage(error)); - } - } - if (args.name) + if (args.name) { try { const extension = extensions.find( (extension) => extension.name === args.name, @@ -99,10 +78,31 @@ export async function handleUpdate(args: UpdateArgs) { } catch (error) { console.error(getErrorMessage(error)); } + } + if (args.all) { + try { + let updateInfos = await updateAllUpdatableExtensions( + workingDir, + extensions, + await checkForAllExtensionUpdates(extensions, new Map(), (_) => {}), + () => {}, + ); + updateInfos = updateInfos.filter( + (info) => info.originalVersion !== info.updatedVersion, + ); + if (updateInfos.length === 0) { + console.log('No extensions to update.'); + return; + } + console.log(updateInfos.map((info) => updateOutput(info)).join('\n')); + } catch (error) { + console.error(getErrorMessage(error)); + } + } } export const updateCommand: CommandModule = { - command: 'update [--all] [name]', + command: 'update [] [--all]', describe: 'Updates all extensions or a named extension to the latest version.', builder: (yargs) => diff --git a/packages/cli/src/config/extensions/github.test.ts b/packages/cli/src/config/extensions/github.test.ts index 5afacd1551..c33d5e84a2 100644 --- a/packages/cli/src/config/extensions/github.test.ts +++ b/packages/cli/src/config/extensions/github.test.ts @@ -128,7 +128,7 @@ describe('git extension helpers', () => { version: '1.0.0', isActive: true, installMetadata: { - type: 'local', + type: 'link', source: '', }, }; diff --git a/packages/cli/src/config/extensions/github.ts b/packages/cli/src/config/extensions/github.ts index 1d85660683..264a2041e3 100644 --- a/packages/cli/src/config/extensions/github.ts +++ b/packages/cli/src/config/extensions/github.ts @@ -16,6 +16,7 @@ import * as https from 'node:https'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { execSync } from 'node:child_process'; +import { loadExtension } from '../extension.js'; function getGitHubToken(): string | undefined { return process.env['GITHUB_TOKEN']; @@ -115,9 +116,29 @@ async function fetchFromGithub( export async function checkForExtensionUpdate( extension: GeminiCLIExtension, setExtensionUpdateState: (updateState: ExtensionUpdateState) => void, + cwd: string = process.cwd(), ): Promise { setExtensionUpdateState(ExtensionUpdateState.CHECKING_FOR_UPDATES); const installMetadata = extension.installMetadata; + if (installMetadata?.type === 'local') { + const newExtension = loadExtension({ + extensionDir: installMetadata.source, + workspaceDir: cwd, + }); + if (!newExtension) { + console.error( + `Failed to check for update for local extension "${extension.name}". Could not load extension from source path: ${installMetadata.source}`, + ); + setExtensionUpdateState(ExtensionUpdateState.ERROR); + return; + } + if (newExtension.config.version !== extension.version) { + setExtensionUpdateState(ExtensionUpdateState.UPDATE_AVAILABLE); + return; + } + setExtensionUpdateState(ExtensionUpdateState.UP_TO_DATE); + return; + } if ( !installMetadata || (installMetadata.type !== 'git' && diff --git a/packages/cli/src/config/extensions/update.test.ts b/packages/cli/src/config/extensions/update.test.ts index 371e5ae279..63af39603c 100644 --- a/packages/cli/src/config/extensions/update.test.ts +++ b/packages/cli/src/config/extensions/update.test.ts @@ -341,17 +341,24 @@ describe('update tests', () => { expect(result).toBe(ExtensionUpdateState.UP_TO_DATE); }); - it('should return NotUpdatable for a non-git extension', async () => { - const extensionDir = createExtension({ + it('should return UpToDate for a local extension with no updates', async () => { + const localExtensionSourcePath = path.join(tempHomeDir, 'local-source'); + const sourceExtensionDir = createExtension({ + extensionsDir: localExtensionSourcePath, + name: 'my-local-ext', + version: '1.0.0', + }); + + const installedExtensionDir = createExtension({ extensionsDir: userExtensionsDir, name: 'local-extension', version: '1.0.0', - installMetadata: { source: '/local/path', type: 'local' }, + installMetadata: { source: sourceExtensionDir, type: 'local' }, }); const extension = annotateActiveExtensions( [ loadExtension({ - extensionDir, + extensionDir: installedExtensionDir, workspaceDir: tempWorkspaceDir, })!, ], @@ -369,9 +376,51 @@ describe('update tests', () => { extensionState = newState; } }, + tempWorkspaceDir, ); const result = results.get('local-extension'); - expect(result).toBe(ExtensionUpdateState.NOT_UPDATABLE); + expect(result).toBe(ExtensionUpdateState.UP_TO_DATE); + }); + + it('should return UpdateAvailable for a local extension with updates', async () => { + const localExtensionSourcePath = path.join(tempHomeDir, 'local-source'); + const sourceExtensionDir = createExtension({ + extensionsDir: localExtensionSourcePath, + name: 'my-local-ext', + version: '1.1.0', + }); + + const installedExtensionDir = createExtension({ + extensionsDir: userExtensionsDir, + name: 'local-extension', + version: '1.0.0', + installMetadata: { source: sourceExtensionDir, type: 'local' }, + }); + const extension = annotateActiveExtensions( + [ + loadExtension({ + extensionDir: installedExtensionDir, + workspaceDir: tempWorkspaceDir, + })!, + ], + [], + process.cwd(), + )[0]; + let extensionState = new Map(); + const results = await checkForAllExtensionUpdates( + [extension], + extensionState, + (newState) => { + if (typeof newState === 'function') { + newState(extensionState); + } else { + extensionState = newState; + } + }, + tempWorkspaceDir, + ); + const result = results.get('local-extension'); + expect(result).toBe(ExtensionUpdateState.UPDATE_AVAILABLE); }); it('should return Error when git check fails', async () => { diff --git a/packages/cli/src/config/extensions/update.ts b/packages/cli/src/config/extensions/update.ts index acb930aee0..17d6771d35 100644 --- a/packages/cli/src/config/extensions/update.ts +++ b/packages/cli/src/config/extensions/update.ts @@ -128,6 +128,7 @@ export async function checkForAllExtensionUpdates( setExtensionsUpdateState: Dispatch< SetStateAction> >, + cwd: string = process.cwd(), ): Promise> { for (const extension of extensions) { const initialState = extensionsUpdateState.get(extension.name); @@ -143,13 +144,17 @@ export async function checkForAllExtensionUpdates( }); continue; } - await checkForExtensionUpdate(extension, (updatedState) => { - setExtensionsUpdateState((prev) => { - extensionsUpdateState = new Map(prev); - extensionsUpdateState.set(extension.name, updatedState); - return extensionsUpdateState; - }); - }); + await checkForExtensionUpdate( + extension, + (updatedState) => { + setExtensionsUpdateState((prev) => { + extensionsUpdateState = new Map(prev); + extensionsUpdateState.set(extension.name, updatedState); + return extensionsUpdateState; + }); + }, + cwd, + ); } } return extensionsUpdateState;