fix(cli): uninstall extensions using their source URL (#8692)

Co-authored-by: Taneja Hriday <hridayt@google.com>
This commit is contained in:
hritan
2025-09-18 16:00:28 +00:00
committed by GitHub
parent 0534ca74d4
commit 2d406ffc75
3 changed files with 47 additions and 10 deletions
@@ -9,7 +9,7 @@ import { uninstallExtension } from '../../config/extension.js';
import { getErrorMessage } from '../../utils/errors.js'; import { getErrorMessage } from '../../utils/errors.js';
interface UninstallArgs { interface UninstallArgs {
name: string; name: string; // can be extension name or source URL.
} }
export async function handleUninstall(args: UninstallArgs) { export async function handleUninstall(args: UninstallArgs) {
@@ -28,7 +28,7 @@ export const uninstallCommand: CommandModule = {
builder: (yargs) => builder: (yargs) =>
yargs yargs
.positional('name', { .positional('name', {
describe: 'The name of the extension to uninstall.', describe: 'The name or source path of the extension to uninstall.',
type: 'string', type: 'string',
}) })
.check((argv) => { .check((argv) => {
+35 -1
View File
@@ -715,7 +715,7 @@ describe('extension tests', () => {
it('should throw an error if the extension does not exist', async () => { it('should throw an error if the extension does not exist', async () => {
await expect(uninstallExtension('nonexistent-extension')).rejects.toThrow( 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'), 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', () => { describe('performWorkspaceExtensionMigration', () => {
+10 -7
View File
@@ -573,17 +573,20 @@ export async function loadExtensionConfig(
} }
export async function uninstallExtension( export async function uninstallExtension(
extensionName: string, extensionIdentifier: string,
cwd: string = process.cwd(), cwd: string = process.cwd(),
): Promise<void> { ): Promise<void> {
const logger = getClearcutLogger(cwd); const logger = getClearcutLogger(cwd);
const installedExtensions = loadUserExtensions(); const installedExtensions = loadUserExtensions();
if ( const extensionName = installedExtensions.find(
!installedExtensions.some( (installed) =>
(installed) => installed.config.name === extensionName, installed.config.name.toLowerCase() ===
) extensionIdentifier.toLowerCase() ||
) { installed.installMetadata?.source.toLowerCase() ===
throw new Error(`Extension "${extensionName}" not found.`); extensionIdentifier.toLowerCase(),
)?.config.name;
if (!extensionName) {
throw new Error(`Extension not found.`);
} }
const manager = new ExtensionEnablementManager( const manager = new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(), ExtensionStorage.getUserExtensionsDir(),