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