Path: blob/main/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../base/common/cancellation.js';6import { getErrorMessage, isCancellationError } from '../../../base/common/errors.js';7import { Schemas } from '../../../base/common/network.js';8import { basename } from '../../../base/common/resources.js';9import { gt } from '../../../base/common/semver/semver.js';10import { URI } from '../../../base/common/uri.js';11import { localize } from '../../../nls.js';12import { EXTENSION_IDENTIFIER_REGEX, IExtensionGalleryService, IExtensionInfo, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions, InstallExtensionInfo, InstallOperation } from './extensionManagement.js';13import { areSameExtensions, getExtensionId, getGalleryExtensionId, getIdAndVersion } from './extensionManagementUtil.js';14import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest } from '../../extensions/common/extensions.js';15import { ILogger } from '../../log/common/log.js';161718const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id);19const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp');2021type InstallVSIXInfo = { vsix: URI; installOptions: InstallOptions };22type InstallGalleryExtensionInfo = { id: string; version?: string; installOptions: InstallOptions };2324export class ExtensionManagementCLI {2526constructor(27protected readonly logger: ILogger,28@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,29@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,30) { }3132protected get location(): string | undefined {33return undefined;34}3536public async listExtensions(showVersions: boolean, category?: string, profileLocation?: URI): Promise<void> {37let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User, profileLocation);38const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase());39if (category && category !== '') {40if (categories.indexOf(category.toLowerCase()) < 0) {41this.logger.info('Invalid category please enter a valid category. To list valid categories run --category without a category specified');42return;43}44extensions = extensions.filter(e => {45if (e.manifest.categories) {46const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase());47return lowerCaseCategories.indexOf(category.toLowerCase()) > -1;48}49return false;50});51} else if (category === '') {52this.logger.info('Possible Categories: ');53categories.forEach(category => {54this.logger.info(category);55});56return;57}58if (this.location) {59this.logger.info(localize('listFromLocation', "Extensions installed on {0}:", this.location));60}6162extensions = extensions.sort((e1, e2) => e1.identifier.id.localeCompare(e2.identifier.id));63let lastId: string | undefined = undefined;64for (const extension of extensions) {65if (lastId !== extension.identifier.id) {66lastId = extension.identifier.id;67this.logger.info(showVersions ? `${lastId}@${extension.manifest.version}` : lastId);68}69}70}7172public async installExtensions(extensions: (string | URI)[], builtinExtensions: (string | URI)[], installOptions: InstallOptions, force: boolean): Promise<void> {73const failed: string[] = [];7475try {76if (extensions.length) {77this.logger.info(this.location ? localize('installingExtensionsOnLocation', "Installing extensions on {0}...", this.location) : localize('installingExtensions', "Installing extensions..."));78}7980const installVSIXInfos: InstallVSIXInfo[] = [];81const installExtensionInfos: InstallGalleryExtensionInfo[] = [];82const addInstallExtensionInfo = (id: string, version: string | undefined, isBuiltin: boolean) => {83installExtensionInfos.push({ id, version: version !== 'prerelease' ? version : undefined, installOptions: { ...installOptions, isBuiltin, installPreReleaseVersion: version === 'prerelease' || installOptions.installPreReleaseVersion } });84};85for (const extension of extensions) {86if (extension instanceof URI) {87installVSIXInfos.push({ vsix: extension, installOptions });88} else {89const [id, version] = getIdAndVersion(extension);90addInstallExtensionInfo(id, version, false);91}92}93for (const extension of builtinExtensions) {94if (extension instanceof URI) {95installVSIXInfos.push({ vsix: extension, installOptions: { ...installOptions, isBuiltin: true, donotIncludePackAndDependencies: true } });96} else {97const [id, version] = getIdAndVersion(extension);98addInstallExtensionInfo(id, version, true);99}100}101102const installed = await this.extensionManagementService.getInstalled(undefined, installOptions.profileLocation);103104if (installVSIXInfos.length) {105await Promise.all(installVSIXInfos.map(async ({ vsix, installOptions }) => {106try {107await this.installVSIX(vsix, installOptions, force, installed);108} catch (err) {109this.logger.error(err);110failed.push(vsix.toString());111}112}));113}114115if (installExtensionInfos.length) {116const failedGalleryExtensions = await this.installGalleryExtensions(installExtensionInfos, installed, force);117failed.push(...failedGalleryExtensions);118}119} catch (error) {120this.logger.error(localize('error while installing extensions', "Error while installing extensions: {0}", getErrorMessage(error)));121throw error;122}123124if (failed.length) {125throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', ')));126}127}128129public async updateExtensions(profileLocation?: URI): Promise<void> {130const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User, profileLocation);131132const installedExtensionsQuery: IExtensionInfo[] = [];133for (const extension of installedExtensions) {134if (!!extension.identifier.uuid) { // No need to check new version for an unpublished extension135installedExtensionsQuery.push({ ...extension.identifier, preRelease: extension.preRelease });136}137}138139this.logger.trace(localize({ key: 'updateExtensionsQuery', comment: ['Placeholder is for the count of extensions'] }, "Fetching latest versions for {0} extensions", installedExtensionsQuery.length));140const availableVersions = await this.extensionGalleryService.getExtensions(installedExtensionsQuery, { compatible: true }, CancellationToken.None);141142const extensionsToUpdate: InstallExtensionInfo[] = [];143for (const newVersion of availableVersions) {144for (const oldVersion of installedExtensions) {145if (areSameExtensions(oldVersion.identifier, newVersion.identifier) && gt(newVersion.version, oldVersion.manifest.version)) {146extensionsToUpdate.push({147extension: newVersion,148options: { operation: InstallOperation.Update, installPreReleaseVersion: oldVersion.preRelease, profileLocation, isApplicationScoped: oldVersion.isApplicationScoped }149});150}151}152}153154if (!extensionsToUpdate.length) {155this.logger.info(localize('updateExtensionsNoExtensions', "No extension to update"));156return;157}158159this.logger.info(localize('updateExtensionsNewVersionsAvailable', "Updating extensions: {0}", extensionsToUpdate.map(ext => ext.extension.identifier.id).join(', ')));160const installationResult = await this.extensionManagementService.installGalleryExtensions(extensionsToUpdate);161162for (const extensionResult of installationResult) {163if (extensionResult.error) {164this.logger.error(localize('errorUpdatingExtension', "Error while updating extension {0}: {1}", extensionResult.identifier.id, getErrorMessage(extensionResult.error)));165} else {166this.logger.info(localize('successUpdate', "Extension '{0}' v{1} was successfully updated.", extensionResult.identifier.id, extensionResult.local?.manifest.version));167}168}169}170171private async installGalleryExtensions(installExtensionInfos: InstallGalleryExtensionInfo[], installed: ILocalExtension[], force: boolean): Promise<string[]> {172installExtensionInfos = installExtensionInfos.filter(installExtensionInfo => {173const { id, version, installOptions } = installExtensionInfo;174const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id }));175if (installedExtension) {176if (!force && (!version || (version === 'prerelease' && installedExtension.preRelease))) {177this.logger.info(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@<version>' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id));178return false;179}180if (version && installedExtension.manifest.version === version) {181this.logger.info(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`));182return false;183}184if (installedExtension.preRelease && version !== 'prerelease') {185installOptions.preRelease = false;186}187}188return true;189});190191if (!installExtensionInfos.length) {192return [];193}194195const failed: string[] = [];196const extensionsToInstall: InstallExtensionInfo[] = [];197const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos);198await Promise.all(installExtensionInfos.map(async ({ id, version, installOptions }) => {199const gallery = galleryExtensions.get(id.toLowerCase());200if (!gallery) {201this.logger.error(`${notFound(version ? `${id}@${version}` : id)}\n${useId}`);202failed.push(id);203return;204}205try {206const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None);207if (manifest && !this.validateExtensionKind(manifest)) {208return;209}210} catch (err) {211this.logger.error(err.message || err.stack || err);212failed.push(id);213return;214}215const installedExtension = installed.find(e => areSameExtensions(e.identifier, gallery.identifier));216if (installedExtension) {217if (gallery.version === installedExtension.manifest.version) {218this.logger.info(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id));219return;220}221this.logger.info(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, gallery.version));222}223if (installOptions.isBuiltin) {224this.logger.info(version ? localize('installing builtin with version', "Installing builtin extension '{0}' v{1}...", id, version) : localize('installing builtin ', "Installing builtin extension '{0}'...", id));225} else {226this.logger.info(version ? localize('installing with version', "Installing extension '{0}' v{1}...", id, version) : localize('installing', "Installing extension '{0}'...", id));227}228extensionsToInstall.push({229extension: gallery,230options: { ...installOptions, installGivenVersion: !!version, isApplicationScoped: installOptions.isApplicationScoped || installedExtension?.isApplicationScoped },231});232}));233234if (extensionsToInstall.length) {235const installationResult = await this.extensionManagementService.installGalleryExtensions(extensionsToInstall);236for (const extensionResult of installationResult) {237if (extensionResult.error) {238this.logger.error(localize('errorInstallingExtension', "Error while installing extension {0}: {1}", extensionResult.identifier.id, getErrorMessage(extensionResult.error)));239failed.push(extensionResult.identifier.id);240} else {241this.logger.info(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", extensionResult.identifier.id, extensionResult.local?.manifest.version));242}243}244}245246return failed;247}248249private async installVSIX(vsix: URI, installOptions: InstallOptions, force: boolean, installedExtensions: ILocalExtension[]): Promise<void> {250251const manifest = await this.extensionManagementService.getManifest(vsix);252if (!manifest) {253throw new Error('Invalid vsix');254}255256const valid = await this.validateVSIX(manifest, force, installOptions.profileLocation, installedExtensions);257if (valid) {258try {259await this.extensionManagementService.install(vsix, { ...installOptions, installGivenVersion: true });260this.logger.info(localize('successVsixInstall', "Extension '{0}' was successfully installed.", basename(vsix)));261} catch (error) {262if (isCancellationError(error)) {263this.logger.info(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", basename(vsix)));264} else {265throw error;266}267}268}269}270271private async getGalleryExtensions(extensions: InstallGalleryExtensionInfo[]): Promise<Map<string, IGalleryExtension>> {272const galleryExtensions = new Map<string, IGalleryExtension>();273const preRelease = extensions.some(e => e.installOptions.installPreReleaseVersion);274const targetPlatform = await this.extensionManagementService.getTargetPlatform();275const extensionInfos: IExtensionInfo[] = [];276for (const extension of extensions) {277if (EXTENSION_IDENTIFIER_REGEX.test(extension.id)) {278extensionInfos.push({ ...extension, preRelease });279}280}281if (extensionInfos.length) {282const result = await this.extensionGalleryService.getExtensions(extensionInfos, { targetPlatform }, CancellationToken.None);283for (const extension of result) {284galleryExtensions.set(extension.identifier.id.toLowerCase(), extension);285}286}287return galleryExtensions;288}289290protected validateExtensionKind(_manifest: IExtensionManifest): boolean {291return true;292}293294private async validateVSIX(manifest: IExtensionManifest, force: boolean, profileLocation: URI | undefined, installedExtensions: ILocalExtension[]): Promise<boolean> {295if (!force) {296const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };297const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && gt(local.manifest.version, manifest.version));298if (newer) {299this.logger.info(localize('forceDowngrade', "A newer version of extension '{0}' v{1} is already installed. Use '--force' option to downgrade to older version.", newer.identifier.id, newer.manifest.version, manifest.version));300return false;301}302}303304return this.validateExtensionKind(manifest);305}306307public async uninstallExtensions(extensions: (string | URI)[], force: boolean, profileLocation?: URI): Promise<void> {308const getId = async (extensionDescription: string | URI): Promise<string> => {309if (extensionDescription instanceof URI) {310const manifest = await this.extensionManagementService.getManifest(extensionDescription);311return getExtensionId(manifest.publisher, manifest.name);312}313return extensionDescription;314};315316const uninstalledExtensions: ILocalExtension[] = [];317for (const extension of extensions) {318const id = await getId(extension);319const installed = await this.extensionManagementService.getInstalled(undefined, profileLocation);320const extensionsToUninstall = installed.filter(e => areSameExtensions(e.identifier, { id }));321if (!extensionsToUninstall.length) {322throw new Error(`${this.notInstalled(id)}\n${useId}`);323}324if (extensionsToUninstall.some(e => e.type === ExtensionType.System)) {325this.logger.info(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be uninstalled", id));326return;327}328if (!force && extensionsToUninstall.some(e => e.isBuiltin)) {329this.logger.info(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id));330return;331}332this.logger.info(localize('uninstalling', "Uninstalling {0}...", id));333for (const extensionToUninstall of extensionsToUninstall) {334await this.extensionManagementService.uninstall(extensionToUninstall, { profileLocation });335uninstalledExtensions.push(extensionToUninstall);336}337338if (this.location) {339this.logger.info(localize('successUninstallFromLocation', "Extension '{0}' was successfully uninstalled from {1}!", id, this.location));340} else {341this.logger.info(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id));342}343344}345}346347public async locateExtension(extensions: string[]): Promise<void> {348const installed = await this.extensionManagementService.getInstalled();349extensions.forEach(e => {350installed.forEach(i => {351if (i.identifier.id === e) {352if (i.location.scheme === Schemas.file) {353this.logger.info(i.location.fsPath);354return;355}356}357});358});359}360361private notInstalled(id: string) {362return this.location ? localize('notInstalleddOnLocation', "Extension '{0}' is not installed on {1}.", id, this.location) : localize('notInstalled', "Extension '{0}' is not installed.", id);363}364365}366367368