Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/extensionManagement/common/extensionManagementCLI.ts
5258 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CancellationToken } from '../../../base/common/cancellation.js';
7
import { getErrorMessage, isCancellationError } from '../../../base/common/errors.js';
8
import { Schemas } from '../../../base/common/network.js';
9
import { basename } from '../../../base/common/resources.js';
10
import { gt } from '../../../base/common/semver/semver.js';
11
import { URI } from '../../../base/common/uri.js';
12
import { localize } from '../../../nls.js';
13
import { EXTENSION_IDENTIFIER_REGEX, IExtensionGalleryService, IExtensionInfo, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions, InstallExtensionInfo, InstallOperation } from './extensionManagement.js';
14
import { areSameExtensions, getExtensionId, getGalleryExtensionId, getIdAndVersion } from './extensionManagementUtil.js';
15
import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest } from '../../extensions/common/extensions.js';
16
import { ILogger } from '../../log/common/log.js';
17
import { IProductService } from '../../product/common/productService.js';
18
19
20
const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id);
21
const useId = localize('useId', "Make sure you use the full extension ID, including the publisher, e.g.: {0}", 'ms-dotnettools.csharp');
22
23
type InstallVSIXInfo = { vsix: URI; installOptions: InstallOptions };
24
type InstallGalleryExtensionInfo = { id: string; version?: string; installOptions: InstallOptions };
25
26
export class ExtensionManagementCLI {
27
28
constructor(
29
private readonly extensionsForceVersionByQuality: readonly string[],
30
protected readonly logger: ILogger,
31
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
32
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
33
@IProductService private readonly productService: IProductService,
34
) {
35
this.extensionsForceVersionByQuality = this.extensionsForceVersionByQuality.map(e => e.toLowerCase());
36
}
37
38
protected get location(): string | undefined {
39
return undefined;
40
}
41
42
public async listExtensions(showVersions: boolean, category?: string, profileLocation?: URI): Promise<void> {
43
let extensions = await this.extensionManagementService.getInstalled(ExtensionType.User, profileLocation);
44
const categories = EXTENSION_CATEGORIES.map(c => c.toLowerCase());
45
if (category && category !== '') {
46
if (categories.indexOf(category.toLowerCase()) < 0) {
47
this.logger.info('Invalid category please enter a valid category. To list valid categories run --category without a category specified');
48
return;
49
}
50
extensions = extensions.filter(e => {
51
if (e.manifest.categories) {
52
const lowerCaseCategories: string[] = e.manifest.categories.map(c => c.toLowerCase());
53
return lowerCaseCategories.indexOf(category.toLowerCase()) > -1;
54
}
55
return false;
56
});
57
} else if (category === '') {
58
this.logger.info('Possible Categories: ');
59
categories.forEach(category => {
60
this.logger.info(category);
61
});
62
return;
63
}
64
if (this.location) {
65
this.logger.info(localize('listFromLocation', "Extensions installed on {0}:", this.location));
66
}
67
68
extensions = extensions.sort((e1, e2) => e1.identifier.id.localeCompare(e2.identifier.id));
69
let lastId: string | undefined = undefined;
70
for (const extension of extensions) {
71
if (lastId !== extension.identifier.id) {
72
lastId = extension.identifier.id;
73
this.logger.info(showVersions ? `${lastId}@${extension.manifest.version}` : lastId);
74
}
75
}
76
}
77
78
public async installExtensions(extensions: (string | URI)[], builtinExtensions: (string | URI)[], installOptions: InstallOptions, force: boolean): Promise<void> {
79
const failed: string[] = [];
80
81
try {
82
if (extensions.length) {
83
this.logger.info(this.location ? localize('installingExtensionsOnLocation', "Installing extensions on {0}...", this.location) : localize('installingExtensions', "Installing extensions..."));
84
}
85
86
const installVSIXInfos: InstallVSIXInfo[] = [];
87
const installExtensionInfos: InstallGalleryExtensionInfo[] = [];
88
const addInstallExtensionInfo = (id: string, version: string | undefined, isBuiltin: boolean) => {
89
if (this.extensionsForceVersionByQuality?.some(e => e === id.toLowerCase())) {
90
version = this.productService.quality !== 'stable' ? 'prerelease' : undefined;
91
}
92
installExtensionInfos.push({ id, version: version !== 'prerelease' ? version : undefined, installOptions: { ...installOptions, isBuiltin, installPreReleaseVersion: version === 'prerelease' || installOptions.installPreReleaseVersion } });
93
};
94
for (const extension of extensions) {
95
if (extension instanceof URI) {
96
installVSIXInfos.push({ vsix: extension, installOptions });
97
} else {
98
const [id, version] = getIdAndVersion(extension);
99
addInstallExtensionInfo(id, version, false);
100
}
101
}
102
for (const extension of builtinExtensions) {
103
if (extension instanceof URI) {
104
installVSIXInfos.push({ vsix: extension, installOptions: { ...installOptions, isBuiltin: true, donotIncludePackAndDependencies: true } });
105
} else {
106
const [id, version] = getIdAndVersion(extension);
107
addInstallExtensionInfo(id, version, true);
108
}
109
}
110
111
const installed = await this.extensionManagementService.getInstalled(undefined, installOptions.profileLocation);
112
113
if (installVSIXInfos.length) {
114
await Promise.all(installVSIXInfos.map(async ({ vsix, installOptions }) => {
115
try {
116
await this.installVSIX(vsix, installOptions, force, installed);
117
} catch (err) {
118
this.logger.error(err);
119
failed.push(vsix.toString());
120
}
121
}));
122
}
123
124
if (installExtensionInfos.length) {
125
const failedGalleryExtensions = await this.installGalleryExtensions(installExtensionInfos, installed, force);
126
failed.push(...failedGalleryExtensions);
127
}
128
} catch (error) {
129
this.logger.error(localize('error while installing extensions', "Error while installing extensions: {0}", getErrorMessage(error)));
130
throw error;
131
}
132
133
if (failed.length) {
134
throw new Error(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', ')));
135
}
136
}
137
138
public async updateExtensions(profileLocation?: URI): Promise<void> {
139
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User, profileLocation);
140
141
const installedExtensionsQuery: IExtensionInfo[] = [];
142
for (const extension of installedExtensions) {
143
if (!!extension.identifier.uuid) { // No need to check new version for an unpublished extension
144
installedExtensionsQuery.push({ ...extension.identifier, preRelease: extension.preRelease });
145
}
146
}
147
148
this.logger.trace(localize({ key: 'updateExtensionsQuery', comment: ['Placeholder is for the count of extensions'] }, "Fetching latest versions for {0} extensions", installedExtensionsQuery.length));
149
const availableVersions = await this.extensionGalleryService.getExtensions(installedExtensionsQuery, { compatible: true }, CancellationToken.None);
150
151
const extensionsToUpdate: InstallExtensionInfo[] = [];
152
for (const newVersion of availableVersions) {
153
for (const oldVersion of installedExtensions) {
154
if (areSameExtensions(oldVersion.identifier, newVersion.identifier) && gt(newVersion.version, oldVersion.manifest.version)) {
155
extensionsToUpdate.push({
156
extension: newVersion,
157
options: { operation: InstallOperation.Update, installPreReleaseVersion: oldVersion.preRelease, profileLocation, isApplicationScoped: oldVersion.isApplicationScoped }
158
});
159
}
160
}
161
}
162
163
if (!extensionsToUpdate.length) {
164
this.logger.info(localize('updateExtensionsNoExtensions', "No extension to update"));
165
return;
166
}
167
168
this.logger.info(localize('updateExtensionsNewVersionsAvailable', "Updating extensions: {0}", extensionsToUpdate.map(ext => ext.extension.identifier.id).join(', ')));
169
const installationResult = await this.extensionManagementService.installGalleryExtensions(extensionsToUpdate);
170
171
for (const extensionResult of installationResult) {
172
if (extensionResult.error) {
173
this.logger.error(localize('errorUpdatingExtension', "Error while updating extension {0}: {1}", extensionResult.identifier.id, getErrorMessage(extensionResult.error)));
174
} else {
175
this.logger.info(localize('successUpdate', "Extension '{0}' v{1} was successfully updated.", extensionResult.identifier.id, extensionResult.local?.manifest.version));
176
}
177
}
178
}
179
180
private async installGalleryExtensions(installExtensionInfos: InstallGalleryExtensionInfo[], installed: ILocalExtension[], force: boolean): Promise<string[]> {
181
installExtensionInfos = installExtensionInfos.filter(installExtensionInfo => {
182
const { id, version, installOptions } = installExtensionInfo;
183
const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id }));
184
if (installedExtension) {
185
if (!force && (!version || (version === 'prerelease' && installedExtension.preRelease))) {
186
this.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));
187
return false;
188
}
189
if (version && installedExtension.manifest.version === version) {
190
this.logger.info(localize('alreadyInstalled', "Extension '{0}' is already installed.", `${id}@${version}`));
191
return false;
192
}
193
if (installedExtension.preRelease && version !== 'prerelease') {
194
installOptions.preRelease = false;
195
}
196
}
197
return true;
198
});
199
200
if (!installExtensionInfos.length) {
201
return [];
202
}
203
204
const failed: string[] = [];
205
const extensionsToInstall: InstallExtensionInfo[] = [];
206
const galleryExtensions = await this.getGalleryExtensions(installExtensionInfos);
207
await Promise.all(installExtensionInfos.map(async ({ id, version, installOptions }) => {
208
const gallery = galleryExtensions.get(id.toLowerCase());
209
if (!gallery) {
210
this.logger.error(`${notFound(version ? `${id}@${version}` : id)}\n${useId}`);
211
failed.push(id);
212
return;
213
}
214
try {
215
const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None);
216
if (manifest && !this.validateExtensionKind(manifest)) {
217
return;
218
}
219
} catch (err) {
220
this.logger.error(err.message || err.stack || err);
221
failed.push(id);
222
return;
223
}
224
const installedExtension = installed.find(e => areSameExtensions(e.identifier, gallery.identifier));
225
if (installedExtension) {
226
if (gallery.version === installedExtension.manifest.version) {
227
this.logger.info(localize('alreadyInstalled', "Extension '{0}' is already installed.", version ? `${id}@${version}` : id));
228
return;
229
}
230
this.logger.info(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, gallery.version));
231
}
232
if (installOptions.isBuiltin) {
233
this.logger.info(version ? localize('installing builtin with version', "Installing builtin extension '{0}' v{1}...", id, version) : localize('installing builtin ', "Installing builtin extension '{0}'...", id));
234
} else {
235
this.logger.info(version ? localize('installing with version', "Installing extension '{0}' v{1}...", id, version) : localize('installing', "Installing extension '{0}'...", id));
236
}
237
extensionsToInstall.push({
238
extension: gallery,
239
options: { ...installOptions, installGivenVersion: !!version, isApplicationScoped: installOptions.isApplicationScoped || installedExtension?.isApplicationScoped },
240
});
241
}));
242
243
if (extensionsToInstall.length) {
244
const installationResult = await this.extensionManagementService.installGalleryExtensions(extensionsToInstall);
245
for (const extensionResult of installationResult) {
246
if (extensionResult.error) {
247
this.logger.error(localize('errorInstallingExtension', "Error while installing extension {0}: {1}", extensionResult.identifier.id, getErrorMessage(extensionResult.error)));
248
failed.push(extensionResult.identifier.id);
249
} else {
250
this.logger.info(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", extensionResult.identifier.id, extensionResult.local?.manifest.version));
251
}
252
}
253
}
254
255
return failed;
256
}
257
258
private async installVSIX(vsix: URI, installOptions: InstallOptions, force: boolean, installedExtensions: ILocalExtension[]): Promise<void> {
259
260
const manifest = await this.extensionManagementService.getManifest(vsix);
261
if (!manifest) {
262
throw new Error('Invalid vsix');
263
}
264
265
const valid = await this.validateVSIX(manifest, force, installOptions.profileLocation, installedExtensions);
266
if (valid) {
267
try {
268
await this.extensionManagementService.install(vsix, { ...installOptions, installGivenVersion: true });
269
this.logger.info(localize('successVsixInstall', "Extension '{0}' was successfully installed.", basename(vsix)));
270
} catch (error) {
271
if (isCancellationError(error)) {
272
this.logger.info(localize('cancelVsixInstall', "Cancelled installing extension '{0}'.", basename(vsix)));
273
} else {
274
throw error;
275
}
276
}
277
}
278
}
279
280
private async getGalleryExtensions(extensions: InstallGalleryExtensionInfo[]): Promise<Map<string, IGalleryExtension>> {
281
const galleryExtensions = new Map<string, IGalleryExtension>();
282
const preRelease = extensions.some(e => e.installOptions.installPreReleaseVersion);
283
const targetPlatform = await this.extensionManagementService.getTargetPlatform();
284
const extensionInfos: IExtensionInfo[] = [];
285
for (const extension of extensions) {
286
if (EXTENSION_IDENTIFIER_REGEX.test(extension.id)) {
287
extensionInfos.push({ ...extension, preRelease });
288
}
289
}
290
if (extensionInfos.length) {
291
const result = await this.extensionGalleryService.getExtensions(extensionInfos, { targetPlatform }, CancellationToken.None);
292
for (const extension of result) {
293
galleryExtensions.set(extension.identifier.id.toLowerCase(), extension);
294
}
295
}
296
return galleryExtensions;
297
}
298
299
protected validateExtensionKind(_manifest: IExtensionManifest): boolean {
300
return true;
301
}
302
303
private async validateVSIX(manifest: IExtensionManifest, force: boolean, profileLocation: URI | undefined, installedExtensions: ILocalExtension[]): Promise<boolean> {
304
if (!force) {
305
const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
306
const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && gt(local.manifest.version, manifest.version));
307
if (newer) {
308
this.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));
309
return false;
310
}
311
}
312
313
return this.validateExtensionKind(manifest);
314
}
315
316
public async uninstallExtensions(extensions: (string | URI)[], force: boolean, profileLocation?: URI): Promise<void> {
317
const getId = async (extensionDescription: string | URI): Promise<string> => {
318
if (extensionDescription instanceof URI) {
319
const manifest = await this.extensionManagementService.getManifest(extensionDescription);
320
return getExtensionId(manifest.publisher, manifest.name);
321
}
322
return extensionDescription;
323
};
324
325
const uninstalledExtensions: ILocalExtension[] = [];
326
for (const extension of extensions) {
327
const id = await getId(extension);
328
const installed = await this.extensionManagementService.getInstalled(undefined, profileLocation);
329
const extensionsToUninstall = installed.filter(e => areSameExtensions(e.identifier, { id }));
330
if (!extensionsToUninstall.length) {
331
throw new Error(`${this.notInstalled(id)}\n${useId}`);
332
}
333
if (extensionsToUninstall.some(e => e.type === ExtensionType.System)) {
334
this.logger.info(localize('builtin', "Extension '{0}' is a Built-in extension and cannot be uninstalled", id));
335
return;
336
}
337
if (!force && extensionsToUninstall.some(e => e.isBuiltin)) {
338
this.logger.info(localize('forceUninstall', "Extension '{0}' is marked as a Built-in extension by user. Please use '--force' option to uninstall it.", id));
339
return;
340
}
341
this.logger.info(localize('uninstalling', "Uninstalling {0}...", id));
342
for (const extensionToUninstall of extensionsToUninstall) {
343
await this.extensionManagementService.uninstall(extensionToUninstall, { profileLocation });
344
uninstalledExtensions.push(extensionToUninstall);
345
}
346
347
if (this.location) {
348
this.logger.info(localize('successUninstallFromLocation', "Extension '{0}' was successfully uninstalled from {1}!", id, this.location));
349
} else {
350
this.logger.info(localize('successUninstall', "Extension '{0}' was successfully uninstalled!", id));
351
}
352
353
}
354
}
355
356
public async locateExtension(extensions: string[]): Promise<void> {
357
const installed = await this.extensionManagementService.getInstalled();
358
extensions.forEach(e => {
359
installed.forEach(i => {
360
if (i.identifier.id === e) {
361
if (i.location.scheme === Schemas.file) {
362
this.logger.info(i.location.fsPath);
363
return;
364
}
365
}
366
});
367
});
368
}
369
370
private notInstalled(id: string) {
371
return this.location ? localize('notInstalleddOnLocation', "Extension '{0}' is not installed on {1}.", id, this.location) : localize('notInstalled', "Extension '{0}' is not installed.", id);
372
}
373
374
}
375
376