Path: blob/main/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts
5258 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';16import { IProductService } from '../../product/common/productService.js';171819const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id);20const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp');2122type InstallVSIXInfo = { vsix: URI; installOptions: InstallOptions };23type InstallGalleryExtensionInfo = { id: string; version?: string; installOptions: InstallOptions };2425export class ExtensionManagementCLI {2627constructor(28private readonly extensionsForceVersionByQuality: readonly string[],29protected readonly logger: ILogger,30@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,31@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,32@IProductService private readonly productService: IProductService,33) {34this.extensionsForceVersionByQuality = this.extensionsForceVersionByQuality.map(e => e.toLowerCase());35}3637protected get location(): string | undefined {38return undefined;39}4041public async listExtensions(showVersions: boolean, category?: string, profileLocation?: URI): Promise<void> {42let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User, profileLocation);43const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase());44if (category && category !== '') {45if (categories.indexOf(category.toLowerCase()) < 0) {46this.logger.info('Invalid category please enter a valid category. To list valid categories run --category without a category specified');47return;48}49extensions = extensions.filter(e => {50if (e.manifest.categories) {51const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase());52return lowerCaseCategories.indexOf(category.toLowerCase()) > -1;53}54return false;55});56} else if (category === '') {57this.logger.info('Possible Categories: ');58categories.forEach(category => {59this.logger.info(category);60});61return;62}63if (this.location) {64this.logger.info(localize('listFromLocation', "Extensions installed on {0}:", this.location));65}6667extensions = extensions.sort((e1, e2) => e1.identifier.id.localeCompare(e2.identifier.id));68let lastId: string | undefined = undefined;69for (const extension of extensions) {70if (lastId !== extension.identifier.id) {71lastId = extension.identifier.id;72this.logger.info(showVersions ? `${lastId}@${extension.manifest.version}` : lastId);73}74}75}7677public async installExtensions(extensions: (string | URI)[], builtinExtensions: (string | URI)[], installOptions: InstallOptions, force: boolean): Promise<void> {78const failed: string[] = [];7980try {81if (extensions.length) {82this.logger.info(this.location ? localize('installingExtensionsOnLocation', "Installing extensions on {0}...", this.location) : localize('installingExtensions', "Installing extensions..."));83}8485const installVSIXInfos: InstallVSIXInfo[] = [];86const installExtensionInfos: InstallGalleryExtensionInfo[] = [];87const addInstallExtensionInfo = (id: string, version: string | undefined, isBuiltin: boolean) => {88if (this.extensionsForceVersionByQuality?.some(e => e === id.toLowerCase())) {89version = this.productService.quality !== 'stable' ? 'prerelease' : undefined;90}91installExtensionInfos.push({ id, version: version !== 'prerelease' ? version : undefined, installOptions: { ...installOptions, isBuiltin, installPreReleaseVersion: version === 'prerelease' || installOptions.installPreReleaseVersion } });92};93for (const extension of extensions) {94if (extension instanceof URI) {95installVSIXInfos.push({ vsix: extension, installOptions });96} else {97const [id, version] = getIdAndVersion(extension);98addInstallExtensionInfo(id, version, false);99}100}101for (const extension of builtinExtensions) {102if (extension instanceof URI) {103installVSIXInfos.push({ vsix: extension, installOptions: { ...installOptions, isBuiltin: true, donotIncludePackAndDependencies: true } });104} else {105const [id, version] = getIdAndVersion(extension);106addInstallExtensionInfo(id, version, true);107}108}109110const installed = await this.extensionManagementService.getInstalled(undefined, installOptions.profileLocation);111112if (installVSIXInfos.length) {113await Promise.all(installVSIXInfos.map(async ({ vsix, installOptions }) => {114try {115await this.installVSIX(vsix, installOptions, force, installed);116} catch (err) {117this.logger.error(err);118failed.push(vsix.toString());119}120}));121}122123if (installExtensionInfos.length) {124const failedGalleryExtensions = await this.installGalleryExtensions(installExtensionInfos, installed, force);125failed.push(...failedGalleryExtensions);126}127} catch (error) {128this.logger.error(localize('error while installing extensions', "Error while installing extensions: {0}", getErrorMessage(error)));129throw error;130}131132if (failed.length) {133throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', ')));134}135}136137public async updateExtensions(profileLocation?: URI): Promise<void> {138const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User, profileLocation);139140const installedExtensionsQuery: IExtensionInfo[] = [];141for (const extension of installedExtensions) {142if (!!extension.identifier.uuid) { // No need to check new version for an unpublished extension143installedExtensionsQuery.push({ ...extension.identifier, preRelease: extension.preRelease });144}145}146147this.logger.trace(localize({ key: 'updateExtensionsQuery', comment: ['Placeholder is for the count of extensions'] }, "Fetching latest versions for {0} extensions", installedExtensionsQuery.length));148const availableVersions = await this.extensionGalleryService.getExtensions(installedExtensionsQuery, { compatible: true }, CancellationToken.None);149150const extensionsToUpdate: InstallExtensionInfo[] = [];151for (const newVersion of availableVersions) {152for (const oldVersion of installedExtensions) {153if (areSameExtensions(oldVersion.identifier, newVersion.identifier) && gt(newVersion.version, oldVersion.manifest.version)) {154extensionsToUpdate.push({155extension: newVersion,156options: { operation: InstallOperation.Update, installPreReleaseVersion: oldVersion.preRelease, profileLocation, isApplicationScoped: oldVersion.isApplicationScoped }157});158}159}160}161162if (!extensionsToUpdate.length) {163this.logger.info(localize('updateExtensionsNoExtensions', "No extension to update"));164return;165}166167this.logger.info(localize('updateExtensionsNewVersionsAvailable', "Updating extensions: {0}", extensionsToUpdate.map(ext => ext.extension.identifier.id).join(', ')));168const installationResult = await this.extensionManagementService.installGalleryExtensions(extensionsToUpdate);169170for (const extensionResult of installationResult) {171if (extensionResult.error) {172this.logger.error(localize('errorUpdatingExtension', "Error while updating extension {0}: {1}", extensionResult.identifier.id, getErrorMessage(extensionResult.error)));173} else {174this.logger.info(localize('successUpdate', "Extension '{0}' v{1} was successfully updated.", extensionResult.identifier.id, extensionResult.local?.manifest.version));175}176}177}178179private async installGalleryExtensions(installExtensionInfos: InstallGalleryExtensionInfo[], installed: ILocalExtension[], force: boolean): Promise<string[]> {180installExtensionInfos = installExtensionInfos.filter(installExtensionInfo => {181const { id, version, installOptions } = installExtensionInfo;182const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id }));183if (installedExtension) {184if (!force && (!version || (version === 'prerelease' && installedExtension.preRelease))) {185this.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));186return false;187}188if (version && installedExtension.manifest.version === version) {189this.logger.info(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`));190return false;191}192if (installedExtension.preRelease && version !== 'prerelease') {193installOptions.preRelease = false;194}195}196return true;197});198199if (!installExtensionInfos.length) {200return [];201}202203const failed: string[] = [];204const extensionsToInstall: InstallExtensionInfo[] = [];205const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos);206await Promise.all(installExtensionInfos.map(async ({ id, version, installOptions }) => {207const gallery = galleryExtensions.get(id.toLowerCase());208if (!gallery) {209this.logger.error(`${notFound(version ? `${id}@${version}` : id)}\n${useId}`);210failed.push(id);211return;212}213try {214const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None);215if (manifest && !this.validateExtensionKind(manifest)) {216return;217}218} catch (err) {219this.logger.error(err.message || err.stack || err);220failed.push(id);221return;222}223const installedExtension = installed.find(e => areSameExtensions(e.identifier, gallery.identifier));224if (installedExtension) {225if (gallery.version === installedExtension.manifest.version) {226this.logger.info(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id));227return;228}229this.logger.info(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, gallery.version));230}231if (installOptions.isBuiltin) {232this.logger.info(version ? localize('installing builtin with version', "Installing builtin extension '{0}' v{1}...", id, version) : localize('installing builtin ', "Installing builtin extension '{0}'...", id));233} else {234this.logger.info(version ? localize('installing with version', "Installing extension '{0}' v{1}...", id, version) : localize('installing', "Installing extension '{0}'...", id));235}236extensionsToInstall.push({237extension: gallery,238options: { ...installOptions, installGivenVersion: !!version, isApplicationScoped: installOptions.isApplicationScoped || installedExtension?.isApplicationScoped },239});240}));241242if (extensionsToInstall.length) {243const installationResult = await this.extensionManagementService.installGalleryExtensions(extensionsToInstall);244for (const extensionResult of installationResult) {245if (extensionResult.error) {246this.logger.error(localize('errorInstallingExtension', "Error while installing extension {0}: {1}", extensionResult.identifier.id, getErrorMessage(extensionResult.error)));247failed.push(extensionResult.identifier.id);248} else {249this.logger.info(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", extensionResult.identifier.id, extensionResult.local?.manifest.version));250}251}252}253254return failed;255}256257private async installVSIX(vsix: URI, installOptions: InstallOptions, force: boolean, installedExtensions: ILocalExtension[]): Promise<void> {258259const manifest = await this.extensionManagementService.getManifest(vsix);260if (!manifest) {261throw new Error('Invalid vsix');262}263264const valid = await this.validateVSIX(manifest, force, installOptions.profileLocation, installedExtensions);265if (valid) {266try {267await this.extensionManagementService.install(vsix, { ...installOptions, installGivenVersion: true });268this.logger.info(localize('successVsixInstall', "Extension '{0}' was successfully installed.", basename(vsix)));269} catch (error) {270if (isCancellationError(error)) {271this.logger.info(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", basename(vsix)));272} else {273throw error;274}275}276}277}278279private async getGalleryExtensions(extensions: InstallGalleryExtensionInfo[]): Promise<Map<string, IGalleryExtension>> {280const galleryExtensions = new Map<string, IGalleryExtension>();281const preRelease = extensions.some(e => e.installOptions.installPreReleaseVersion);282const targetPlatform = await this.extensionManagementService.getTargetPlatform();283const extensionInfos: IExtensionInfo[] = [];284for (const extension of extensions) {285if (EXTENSION_IDENTIFIER_REGEX.test(extension.id)) {286extensionInfos.push({ ...extension, preRelease });287}288}289if (extensionInfos.length) {290const result = await this.extensionGalleryService.getExtensions(extensionInfos, { targetPlatform }, CancellationToken.None);291for (const extension of result) {292galleryExtensions.set(extension.identifier.id.toLowerCase(), extension);293}294}295return galleryExtensions;296}297298protected validateExtensionKind(_manifest: IExtensionManifest): boolean {299return true;300}301302private async validateVSIX(manifest: IExtensionManifest, force: boolean, profileLocation: URI | undefined, installedExtensions: ILocalExtension[]): Promise<boolean> {303if (!force) {304const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };305const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && gt(local.manifest.version, manifest.version));306if (newer) {307this.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));308return false;309}310}311312return this.validateExtensionKind(manifest);313}314315public async uninstallExtensions(extensions: (string | URI)[], force: boolean, profileLocation?: URI): Promise<void> {316const getId = async (extensionDescription: string | URI): Promise<string> => {317if (extensionDescription instanceof URI) {318const manifest = await this.extensionManagementService.getManifest(extensionDescription);319return getExtensionId(manifest.publisher, manifest.name);320}321return extensionDescription;322};323324const uninstalledExtensions: ILocalExtension[] = [];325for (const extension of extensions) {326const id = await getId(extension);327const installed = await this.extensionManagementService.getInstalled(undefined, profileLocation);328const extensionsToUninstall = installed.filter(e => areSameExtensions(e.identifier, { id }));329if (!extensionsToUninstall.length) {330throw new Error(`${this.notInstalled(id)}\n${useId}`);331}332if (extensionsToUninstall.some(e => e.type === ExtensionType.System)) {333this.logger.info(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be uninstalled", id));334return;335}336if (!force && extensionsToUninstall.some(e => e.isBuiltin)) {337this.logger.info(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id));338return;339}340this.logger.info(localize('uninstalling', "Uninstalling {0}...", id));341for (const extensionToUninstall of extensionsToUninstall) {342await this.extensionManagementService.uninstall(extensionToUninstall, { profileLocation });343uninstalledExtensions.push(extensionToUninstall);344}345346if (this.location) {347this.logger.info(localize('successUninstallFromLocation', "Extension '{0}' was successfully uninstalled from {1}!", id, this.location));348} else {349this.logger.info(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id));350}351352}353}354355public async locateExtension(extensions: string[]): Promise<void> {356const installed = await this.extensionManagementService.getInstalled();357extensions.forEach(e => {358installed.forEach(i => {359if (i.identifier.id === e) {360if (i.location.scheme === Schemas.file) {361this.logger.info(i.location.fsPath);362return;363}364}365});366});367}368369private notInstalled(id: string) {370return this.location ? localize('notInstalleddOnLocation', "Extension '{0}' is not installed on {1}.", id, this.location) : localize('notInstalled', "Extension '{0}' is not installed.", id);371}372373}374375376