From ee2b645d7efd81037ad70249419b2606de181b87 Mon Sep 17 00:00:00 2001 From: Taneja Hriday Date: Thu, 18 Sep 2025 13:38:33 +0000 Subject: [PATCH 1/2] fix(cli): uninstall extensions using their source URL --- .../cli/src/commands/extensions/uninstall.ts | 18 +++++----- packages/cli/src/config/extension.test.ts | 36 ++++++++++++++++++- packages/cli/src/config/extension.ts | 15 ++++---- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/commands/extensions/uninstall.ts b/packages/cli/src/commands/extensions/uninstall.ts index ff93b79723f..096e250cde4 100644 --- a/packages/cli/src/commands/extensions/uninstall.ts +++ b/packages/cli/src/commands/extensions/uninstall.ts @@ -9,13 +9,13 @@ import { uninstallExtension } from '../../config/extension.js'; import { getErrorMessage } from '../../utils/errors.js'; interface UninstallArgs { - name: string; + identifier: string; // can be extension name or source URL. } export async function handleUninstall(args: UninstallArgs) { try { - await uninstallExtension(args.name); - console.log(`Extension "${args.name}" successfully uninstalled.`); + await uninstallExtension(args.identifier); + console.log(`Extension "${args.identifier}" successfully uninstalled.`); } catch (error) { console.error(getErrorMessage(error)); process.exit(1); @@ -23,25 +23,25 @@ export async function handleUninstall(args: UninstallArgs) { } export const uninstallCommand: CommandModule = { - command: 'uninstall ', + command: 'uninstall ', describe: 'Uninstalls an extension.', builder: (yargs) => yargs - .positional('name', { - describe: 'The name of the extension to uninstall.', + .positional('identifier', { + describe: 'The identifier of the extension to uninstall.', type: 'string', }) .check((argv) => { - if (!argv.name) { + if (!argv.identifier) { throw new Error( - 'Please include the name of the extension to uninstall as a positional argument.', + 'Please include the identifier of the extension to uninstall as a positional argument.', ); } return true; }), handler: async (argv) => { await handleUninstall({ - name: argv['name'] as string, + identifier: argv['identifier'] as string, }); }, }; diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index fda8e582793..28ea16bf1c5 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -715,7 +715,7 @@ describe('extension tests', () => { it('should throw an error if the extension does not exist', async () => { await expect(uninstallExtension('nonexistent-extension')).rejects.toThrow( - 'Extension "nonexistent-extension" not found.', + 'Extension not found.', ); }); @@ -733,6 +733,40 @@ describe('extension tests', () => { new ExtensionUninstallEvent('my-local-extension', 'success'), ); }); + + it('should uninstall an extension by its source URL', async () => { + const gitUrl = 'https://github.com/google/gemini-sql-extension.git'; + const sourceExtDir = createExtension({ + extensionsDir: userExtensionsDir, + name: 'gemini-sql-extension', + version: '1.0.0', + installMetadata: { + source: gitUrl, + type: 'git', + }, + }); + + await uninstallExtension(gitUrl); + + expect(fs.existsSync(sourceExtDir)).toBe(false); + const logger = ClearcutLogger.getInstance({} as Config); + expect(logger?.logExtensionUninstallEvent).toHaveBeenCalledWith( + new ExtensionUninstallEvent('gemini-sql-extension', 'success'), + ); + }); + + it('should fail to uninstall by URL if an extension has no install metadata', async () => { + createExtension({ + extensionsDir: userExtensionsDir, + name: 'no-metadata-extension', + version: '1.0.0', + // No installMetadata provided + }); + + await expect( + uninstallExtension('https://github.com/google/no-metadata-extension'), + ).rejects.toThrow('Extension not found.'); + }); }); describe('performWorkspaceExtensionMigration', () => { diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index c187bcb38e5..779d893a1a8 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -573,17 +573,18 @@ export async function loadExtensionConfig( } export async function uninstallExtension( - extensionName: string, + extensionIdentifier: string, cwd: string = process.cwd(), ): Promise { const logger = getClearcutLogger(cwd); const installedExtensions = loadUserExtensions(); - if ( - !installedExtensions.some( - (installed) => installed.config.name === extensionName, - ) - ) { - throw new Error(`Extension "${extensionName}" not found.`); + const extensionName = installedExtensions.find( + (installed) => + installed.config.name === extensionIdentifier || + installed.installMetadata?.source === extensionIdentifier, + )?.config.name; + if (!extensionName) { + throw new Error(`Extension not found.`); } const manager = new ExtensionEnablementManager( ExtensionStorage.getUserExtensionsDir(), From ecbe01f166f171245394531d5c7dc3e38f21ee80 Mon Sep 17 00:00:00 2001 From: Taneja Hriday Date: Thu, 18 Sep 2025 15:07:45 +0000 Subject: [PATCH 2/2] review comments --- .../cli/src/commands/extensions/uninstall.ts | 18 +++++++++--------- packages/cli/src/config/extension.ts | 6 ++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/extensions/uninstall.ts b/packages/cli/src/commands/extensions/uninstall.ts index 096e250cde4..d7c131962be 100644 --- a/packages/cli/src/commands/extensions/uninstall.ts +++ b/packages/cli/src/commands/extensions/uninstall.ts @@ -9,13 +9,13 @@ import { uninstallExtension } from '../../config/extension.js'; import { getErrorMessage } from '../../utils/errors.js'; interface UninstallArgs { - identifier: string; // can be extension name or source URL. + name: string; // can be extension name or source URL. } export async function handleUninstall(args: UninstallArgs) { try { - await uninstallExtension(args.identifier); - console.log(`Extension "${args.identifier}" successfully uninstalled.`); + await uninstallExtension(args.name); + console.log(`Extension "${args.name}" successfully uninstalled.`); } catch (error) { console.error(getErrorMessage(error)); process.exit(1); @@ -23,25 +23,25 @@ export async function handleUninstall(args: UninstallArgs) { } export const uninstallCommand: CommandModule = { - command: 'uninstall ', + command: 'uninstall ', describe: 'Uninstalls an extension.', builder: (yargs) => yargs - .positional('identifier', { - describe: 'The identifier of the extension to uninstall.', + .positional('name', { + describe: 'The name or source path of the extension to uninstall.', type: 'string', }) .check((argv) => { - if (!argv.identifier) { + if (!argv.name) { throw new Error( - 'Please include the identifier of the extension to uninstall as a positional argument.', + 'Please include the name of the extension to uninstall as a positional argument.', ); } return true; }), handler: async (argv) => { await handleUninstall({ - identifier: argv['identifier'] as string, + name: argv['name'] as string, }); }, }; diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 779d893a1a8..8205efa011c 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -580,8 +580,10 @@ export async function uninstallExtension( const installedExtensions = loadUserExtensions(); const extensionName = installedExtensions.find( (installed) => - installed.config.name === extensionIdentifier || - installed.installMetadata?.source === extensionIdentifier, + installed.config.name.toLowerCase() === + extensionIdentifier.toLowerCase() || + installed.installMetadata?.source.toLowerCase() === + extensionIdentifier.toLowerCase(), )?.config.name; if (!extensionName) { throw new Error(`Extension not found.`);