Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/pluginSources.ts
13401 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 { Action } from '../../../../base/common/actions.js';
7
import { CancelablePromise, timeout } from '../../../../base/common/async.js';
8
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
9
import { Event } from '../../../../base/common/event.js';
10
import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
11
import { isWindows } from '../../../../base/common/platform.js';
12
import { dirname, isEqualOrParent, joinPath } from '../../../../base/common/resources.js';
13
import { URI } from '../../../../base/common/uri.js';
14
import { localize } from '../../../../nls.js';
15
import { ICommandService } from '../../../../platform/commands/common/commands.js';
16
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
17
import { IFileService } from '../../../../platform/files/common/files.js';
18
import { ILogService } from '../../../../platform/log/common/log.js';
19
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
20
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
21
import { TerminalCapability, type ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js';
22
import { ITerminalInstance, ITerminalService } from '../../terminal/browser/terminal.js';
23
import { IEnsureRepositoryOptions, IPullRepositoryOptions } from '../common/plugins/agentPluginRepositoryService.js';
24
import { IGitHubPluginSource, IGitUrlPluginSource, IMarketplacePlugin, INpmPluginSource, IPipPluginSource, IPluginSourceDescriptor, PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js';
25
import { IPluginSource } from '../common/plugins/pluginSource.js';
26
import { IPluginGitService } from '../common/plugins/pluginGitService.js';
27
28
// ---------------------------------------------------------------------------
29
// Shared helpers
30
// ---------------------------------------------------------------------------
31
32
function sanitizeCacheSegment(name: string): string {
33
return name.replace(/[\\/:*?"<>|]/g, '_');
34
}
35
36
function gitRevisionCacheSuffix(ref?: string, sha?: string): string[] {
37
if (sha) {
38
return [`sha_${sanitizeCacheSegment(sha)}`];
39
}
40
if (ref) {
41
return [`ref_${sanitizeCacheSegment(ref)}`];
42
}
43
return [];
44
}
45
46
function shellEscapeArg(value: string): string {
47
if (isWindows) {
48
return `"${value.replace(/[`$"]/g, '`$&')}"`;
49
}
50
return `'${value.replace(/'/g, `'\\''`)}'`;
51
}
52
53
function formatShellCommand(args: readonly string[]): string {
54
const [command, ...rest] = args;
55
return [command, ...rest.map(arg => shellEscapeArg(arg))].join(' ');
56
}
57
58
// ---------------------------------------------------------------------------
59
// Base for git-based sources (GitHub shorthand & arbitrary Git URL)
60
// ---------------------------------------------------------------------------
61
62
abstract class AbstractGitPluginSource implements IPluginSource {
63
abstract readonly kind: PluginSourceKind;
64
constructor(
65
@ICommandService protected readonly _commandService: ICommandService,
66
@IFileService protected readonly _fileService: IFileService,
67
@ILogService protected readonly _logService: ILogService,
68
@INotificationService protected readonly _notificationService: INotificationService,
69
@IPluginGitService protected readonly _pluginGit: IPluginGitService,
70
@IProgressService protected readonly _progressService: IProgressService,
71
) { }
72
73
abstract getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI;
74
abstract getLabel(descriptor: IPluginSourceDescriptor): string;
75
protected abstract _cloneUrl(descriptor: IPluginSourceDescriptor): string;
76
protected abstract _displayLabel(descriptor: IPluginSourceDescriptor): string;
77
78
getCleanupTarget(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI | undefined {
79
return this._getRepoDir(cacheRoot, descriptor);
80
}
81
82
/**
83
* Returns the on-disk directory of the cloned repository. Subclasses that
84
* support a sub-path within a repository should override this to return the
85
* repository root, while {@link getInstallUri} returns root + sub-path.
86
*/
87
protected _getRepoDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI {
88
return this.getInstallUri(cacheRoot, descriptor);
89
}
90
91
async ensure(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise<URI> {
92
const descriptor = plugin.sourceDescriptor;
93
const repoDir = this._getRepoDir(cacheRoot, descriptor);
94
const repoExists = await this._fileService.exists(repoDir);
95
const label = this._displayLabel(descriptor);
96
97
if (repoExists) {
98
await this._checkoutRevision(repoDir, descriptor, options?.failureLabel ?? label);
99
return this.getInstallUri(cacheRoot, descriptor);
100
}
101
102
const progressTitle = options?.progressTitle ?? localize('cloningPluginSource', "Cloning plugin source '{0}'...", label);
103
const failureLabel = options?.failureLabel ?? label;
104
const ref = (descriptor as IGitHubPluginSource | IGitUrlPluginSource).ref;
105
106
await this._cloneRepository(repoDir, this._cloneUrl(descriptor), progressTitle, failureLabel, ref);
107
await this._checkoutRevision(repoDir, descriptor, failureLabel);
108
return this.getInstallUri(cacheRoot, descriptor);
109
}
110
111
async update(cacheRoot: URI, plugin: IMarketplacePlugin, options?: IPullRepositoryOptions): Promise<boolean> {
112
const descriptor = plugin.sourceDescriptor;
113
const repoDir = this._getRepoDir(cacheRoot, descriptor);
114
const repoExists = await this._fileService.exists(repoDir);
115
if (!repoExists) {
116
this._logService.warn(`[${this.kind}] Cannot update plugin '${options?.pluginName ?? plugin.name}': source repository not cloned`);
117
return false;
118
}
119
120
const updateLabel = options?.pluginName ?? plugin.name;
121
const failureLabel = options?.failureLabel ?? updateLabel;
122
123
try {
124
const doUpdate = async (cts?: CancellationTokenSource) => {
125
const git = descriptor as IGitHubPluginSource | IGitUrlPluginSource;
126
let changed: boolean;
127
if (git.sha) {
128
const headBefore = await this._pluginGit.revParse(repoDir, 'HEAD').catch(() => undefined);
129
await this._pluginGit.fetch(repoDir, cts?.token);
130
await this._checkoutRevision(repoDir, descriptor, failureLabel, cts?.token);
131
const headAfter = await this._pluginGit.revParse(repoDir, 'HEAD').catch(() => undefined);
132
changed = headBefore !== headAfter;
133
} else {
134
changed = await this._pluginGit.pull(repoDir, cts?.token);
135
await this._checkoutRevision(repoDir, descriptor, failureLabel, cts?.token);
136
}
137
return changed;
138
};
139
140
if (options?.silent) {
141
return await doUpdate();
142
}
143
144
const cts = new CancellationTokenSource();
145
try {
146
return await this._progressService.withProgress(
147
{
148
location: ProgressLocation.Notification,
149
title: localize('updatingPluginSource', "Updating plugin '{0}'...", updateLabel),
150
cancellable: true,
151
},
152
() => doUpdate(cts),
153
() => cts.dispose(true),
154
);
155
} finally {
156
cts.dispose();
157
}
158
} catch (err) {
159
this._logService.error(`[${this.kind}] Failed to update plugin source '${updateLabel}':`, err);
160
if (!options?.silent) {
161
this._notificationService.notify({
162
severity: Severity.Error,
163
message: localize('pullPluginSourceFailed', "Failed to update plugin '{0}': {1}", failureLabel, err?.message ?? String(err)),
164
});
165
}
166
throw err;
167
}
168
}
169
170
// -- internal helpers ---
171
172
private async _cloneRepository(repoDir: URI, cloneUrl: string, progressTitle: string, failureLabel: string, ref?: string): Promise<void> {
173
const cts = new CancellationTokenSource();
174
try {
175
await this._progressService.withProgress(
176
{
177
location: ProgressLocation.Notification,
178
title: progressTitle,
179
cancellable: true,
180
},
181
async () => {
182
await this._fileService.createFolder(dirname(repoDir));
183
await this._pluginGit.cloneRepository(cloneUrl, repoDir, ref, cts.token);
184
},
185
() => cts.dispose(true),
186
);
187
} catch (err) {
188
this._logService.error(`[${this.kind}] Failed to clone ${cloneUrl}:`, err);
189
this._notificationService.notify({
190
severity: Severity.Error,
191
message: localize('cloneFailed', "Failed to install plugin '{0}': {1}", failureLabel, err?.message ?? String(err)),
192
});
193
throw err;
194
} finally {
195
cts.dispose();
196
}
197
}
198
199
private async _checkoutRevision(repoDir: URI, descriptor: IPluginSourceDescriptor, failureLabel: string, token?: CancellationToken): Promise<void> {
200
const git = descriptor as IGitHubPluginSource | IGitUrlPluginSource;
201
if (!git.sha && !git.ref) {
202
return;
203
}
204
205
try {
206
if (git.sha) {
207
await this._pluginGit.checkout(repoDir, git.sha, true, token);
208
return;
209
}
210
// git.ref is guaranteed non-nullish by the guard above
211
await this._pluginGit.checkout(repoDir, git.ref!, undefined, token);
212
} catch (err) {
213
this._logService.error(`[${this.kind}] Failed to checkout revision for '${failureLabel}':`, err);
214
this._notificationService.notify({
215
severity: Severity.Error,
216
message: localize('checkoutPluginSourceFailed', "Failed to checkout plugin '{0}' to requested revision: {1}", failureLabel, err?.message ?? String(err)),
217
});
218
throw err;
219
}
220
}
221
}
222
223
// ---------------------------------------------------------------------------
224
// RelativePath — plugin lives inside a shared marketplace repository
225
// ---------------------------------------------------------------------------
226
227
export class RelativePathPluginSource implements IPluginSource {
228
readonly kind = PluginSourceKind.RelativePath;
229
230
getInstallUri(_cacheRoot: URI, _descriptor: IPluginSourceDescriptor): URI {
231
throw new Error('Use getPluginInstallUri() for relative-path sources');
232
}
233
234
async ensure(_cacheRoot: URI, _plugin: IMarketplacePlugin, _options?: IEnsureRepositoryOptions): Promise<URI> {
235
throw new Error('Use ensureRepository() for relative-path sources');
236
}
237
238
async update(_cacheRoot: URI, _plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise<boolean> {
239
throw new Error('Use pullRepository() for relative-path sources');
240
}
241
242
getCleanupTarget(_cacheRoot: URI, _descriptor: IPluginSourceDescriptor): URI | undefined {
243
return undefined;
244
}
245
246
getLabel(descriptor: IPluginSourceDescriptor): string {
247
return (descriptor as { path: string }).path || '.';
248
}
249
}
250
251
// ---------------------------------------------------------------------------
252
// GitHub — `{ source: "github", repo: "owner/repo" }`
253
// ---------------------------------------------------------------------------
254
255
export class GitHubPluginSource extends AbstractGitPluginSource {
256
readonly kind = PluginSourceKind.GitHub;
257
258
/** Returns the URI where the plugin content lives (repo root + optional sub-path). */
259
getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI {
260
const repoDir = this._getRepoDir(cacheRoot, descriptor);
261
const gh = descriptor as IGitHubPluginSource;
262
if (gh.path) {
263
const normalizedPath = gh.path.trim().replace(/^\.?\/+|\/+$/g, '');
264
if (normalizedPath) {
265
const target = joinPath(repoDir, normalizedPath);
266
if (isEqualOrParent(target, repoDir)) {
267
return target;
268
}
269
}
270
}
271
return repoDir;
272
}
273
274
/** Returns the cloned repository root (without sub-path). */
275
protected override _getRepoDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI {
276
const gh = descriptor as IGitHubPluginSource;
277
const [owner, repo] = gh.repo.split('/');
278
return joinPath(cacheRoot, 'github.com', owner, repo, ...gitRevisionCacheSuffix(gh.ref, gh.sha));
279
}
280
281
getLabel(descriptor: IPluginSourceDescriptor): string {
282
const gh = descriptor as IGitHubPluginSource;
283
return gh.path ? `${gh.repo}/${gh.path}` : gh.repo;
284
}
285
286
protected _cloneUrl(descriptor: IPluginSourceDescriptor): string {
287
return `https://github.com/${(descriptor as IGitHubPluginSource).repo}.git`;
288
}
289
290
protected _displayLabel(descriptor: IPluginSourceDescriptor): string {
291
return (descriptor as IGitHubPluginSource).repo;
292
}
293
}
294
295
// ---------------------------------------------------------------------------
296
// GitUrl — `{ source: "url", url: "https://…/repo.git" }`
297
// ---------------------------------------------------------------------------
298
299
export class GitUrlPluginSource extends AbstractGitPluginSource {
300
readonly kind = PluginSourceKind.GitUrl;
301
302
getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI {
303
const git = descriptor as IGitUrlPluginSource;
304
const segments = this._gitUrlCacheSegments(git.url, git.ref, git.sha);
305
return joinPath(cacheRoot, ...segments);
306
}
307
308
getLabel(descriptor: IPluginSourceDescriptor): string {
309
return (descriptor as IGitUrlPluginSource).url;
310
}
311
312
protected _cloneUrl(descriptor: IPluginSourceDescriptor): string {
313
return (descriptor as IGitUrlPluginSource).url;
314
}
315
316
protected _displayLabel(descriptor: IPluginSourceDescriptor): string {
317
return (descriptor as IGitUrlPluginSource).url;
318
}
319
320
private _gitUrlCacheSegments(url: string, ref?: string, sha?: string): string[] {
321
try {
322
const parsed = URI.parse(url);
323
const authority = (parsed.authority || 'unknown').replace(/[\\/:*?"<>|]/g, '_').toLowerCase();
324
const pathPart = parsed.path.replace(/^\/+/, '').replace(/\.git$/i, '').replace(/\/+$/g, '');
325
const segments = pathPart.split('/').map(s => s.replace(/[\\/:*?"<>|]/g, '_'));
326
return [authority, ...segments, ...gitRevisionCacheSuffix(ref, sha)];
327
} catch {
328
return ['git', url.replace(/[\\/:*?"<>|]/g, '_'), ...gitRevisionCacheSuffix(ref, sha)];
329
}
330
}
331
}
332
333
// ---------------------------------------------------------------------------
334
// Base for package-manager-based sources (npm, pip)
335
// ---------------------------------------------------------------------------
336
337
export abstract class AbstractPackagePluginSource implements IPluginSource {
338
abstract readonly kind: PluginSourceKind;
339
constructor(
340
@IDialogService protected readonly _dialogService: IDialogService,
341
@IFileService protected readonly _fileService: IFileService,
342
@ILogService protected readonly _logService: ILogService,
343
@INotificationService protected readonly _notificationService: INotificationService,
344
@IProgressService protected readonly _progressService: IProgressService,
345
@ITerminalService protected readonly _terminalService: ITerminalService,
346
) { }
347
348
abstract getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI;
349
abstract getLabel(descriptor: IPluginSourceDescriptor): string;
350
351
getCleanupTarget(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI | undefined {
352
return this._getCacheDir(cacheRoot, descriptor);
353
}
354
355
/**
356
* Return the parent directory (prefix / target) where the package
357
* manager installs into. This is above the actual plugin content dir.
358
*/
359
protected abstract _getCacheDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI;
360
361
/** Build the terminal command args for install. */
362
protected abstract _buildInstallArgs(installDir: URI, plugin: IMarketplacePlugin): string[];
363
364
/** Human-readable package manager name for messages. */
365
protected abstract get _managerName(): string;
366
367
async ensure(cacheRoot: URI, plugin: IMarketplacePlugin, _options?: IEnsureRepositoryOptions): Promise<URI> {
368
const cacheDir = this._getCacheDir(cacheRoot, plugin.sourceDescriptor);
369
await this._fileService.createFolder(cacheDir);
370
return cacheDir;
371
}
372
373
async update(cacheRoot: URI, plugin: IMarketplacePlugin, _options?: IPullRepositoryOptions): Promise<boolean> {
374
// For package-manager sources, "update" re-runs install.
375
const installDir = this._getCacheDir(cacheRoot, plugin.sourceDescriptor);
376
const pluginDir = this.getInstallUri(cacheRoot, plugin.sourceDescriptor);
377
await this.runInstall(installDir, pluginDir, plugin, { silent: _options?.silent });
378
return true;
379
}
380
381
async runInstall(installDir: URI, pluginDir: URI, plugin: IMarketplacePlugin, options?: { silent?: boolean }): Promise<{ pluginDir: URI } | undefined> {
382
const args = this._buildInstallArgs(installDir, plugin);
383
const command = formatShellCommand(args);
384
const confirmed = await this._confirmTerminalCommand(plugin.name, command, options?.silent);
385
if (!confirmed) {
386
return undefined;
387
}
388
389
const progressTitle = localize('installingPackagePlugin', "Installing {0} plugin '{1}'...", this._managerName, plugin.name);
390
const { success, terminal } = await this._runTerminalCommand(command, progressTitle);
391
if (!success) {
392
return undefined;
393
}
394
395
const exists = await this._fileService.exists(pluginDir);
396
if (!exists) {
397
this._notificationService.notify({
398
severity: Severity.Error,
399
message: localize('packagePluginNotFound', "{0} package '{1}' was not found after installation.", this._managerName, this.getLabel(plugin.sourceDescriptor)),
400
});
401
return undefined;
402
}
403
404
terminal?.dispose();
405
return { pluginDir };
406
}
407
408
// -- terminal helpers (moved from PluginInstallService) ---
409
410
private async _confirmTerminalCommand(pluginName: string, command: string, silent?: boolean): Promise<boolean> {
411
if (silent) {
412
return new Promise<boolean>(resolve => {
413
const n = this._notificationService.notify({
414
severity: Severity.Info,
415
message: localize('confirmPluginInstallNotification', "Plugin '{0}' wants to run: {1}", pluginName, command),
416
actions: {
417
primary: [
418
new Action('installPlugin', localize('install', "Install"), undefined, true, async () => resolve(true)),
419
],
420
},
421
});
422
423
Event.once(n.onDidClose)(() => resolve(false));
424
});
425
}
426
427
const { confirmed } = await this._dialogService.confirm({
428
type: 'question',
429
message: localize('confirmPluginInstall', "Install Plugin '{0}'?", pluginName),
430
detail: localize('confirmPluginInstallDetail', "This will run the following command in a terminal:\n\n{0}", command),
431
primaryButton: localize({ key: 'confirmInstall', comment: ['&& denotes a mnemonic'] }, "&&Install"),
432
});
433
return confirmed;
434
}
435
436
private async _runTerminalCommand(command: string, progressTitle: string) {
437
let terminal: ITerminalInstance | undefined;
438
try {
439
await this._progressService.withProgress(
440
{
441
location: ProgressLocation.Notification,
442
title: progressTitle,
443
cancellable: false,
444
},
445
async () => {
446
terminal = await this._terminalService.createTerminal({
447
config: {
448
name: localize('pluginInstallTerminal', "Plugin Install"),
449
forceShellIntegration: true,
450
isTransient: true,
451
isFeatureTerminal: true,
452
},
453
});
454
await terminal.processReady;
455
this._terminalService.setActiveInstance(terminal);
456
457
const commandResultPromise = this._waitForTerminalCommandCompletion(terminal);
458
await terminal.runCommand(command, true);
459
const exitCode = await commandResultPromise;
460
if (exitCode !== 0) {
461
throw new Error(localize('terminalCommandExitCode', "Command exited with code {0}", exitCode));
462
}
463
}
464
);
465
return { success: true, terminal };
466
} catch (err) {
467
this._logService.error(`[${this.kind}] Terminal command failed:`, err);
468
this._notificationService.notify({
469
severity: Severity.Error,
470
message: localize('terminalCommandFailed', "Plugin installation command failed: {0}", err?.message ?? String(err)),
471
});
472
return { success: false, terminal };
473
}
474
}
475
476
private _waitForTerminalCommandCompletion(terminal: ITerminalInstance): Promise<number | undefined> {
477
return new Promise<number | undefined>(resolve => {
478
const disposables = new DisposableStore();
479
let isResolved = false;
480
481
const resolveAndDispose = (exitCode: number | undefined): void => {
482
if (isResolved) {
483
return;
484
}
485
isResolved = true;
486
disposables.dispose();
487
resolve(exitCode);
488
};
489
490
const attachCommandFinishedListener = (): void => {
491
const commandDetection = terminal.capabilities.get(TerminalCapability.CommandDetection);
492
if (!commandDetection) {
493
return;
494
}
495
disposables.add(commandDetection.onCommandFinished((command: ITerminalCommand) => {
496
resolveAndDispose(command.exitCode ?? 0);
497
}));
498
};
499
500
attachCommandFinishedListener();
501
disposables.add(terminal.capabilities.onDidAddCommandDetectionCapability(() => attachCommandFinishedListener()));
502
503
const timeoutHandle: CancelablePromise<void> = timeout(120_000);
504
disposables.add(toDisposable(() => timeoutHandle.cancel()));
505
void timeoutHandle.then(() => {
506
if (isResolved) {
507
return;
508
}
509
this._logService.warn(`[${this.kind}] Terminal command completion timed out`);
510
resolveAndDispose(undefined);
511
});
512
});
513
}
514
}
515
516
// ---------------------------------------------------------------------------
517
// npm — `{ source: "npm", package: "@org/plugin" }`
518
// ---------------------------------------------------------------------------
519
520
export class NpmPluginSource extends AbstractPackagePluginSource {
521
readonly kind = PluginSourceKind.Npm;
522
protected readonly _managerName = 'npm';
523
524
getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI {
525
const npm = descriptor as INpmPluginSource;
526
return joinPath(cacheRoot, 'npm', sanitizeCacheSegment(npm.package), 'node_modules', npm.package);
527
}
528
529
getLabel(descriptor: IPluginSourceDescriptor): string {
530
const npm = descriptor as INpmPluginSource;
531
return npm.version ? `${npm.package}@${npm.version}` : npm.package;
532
}
533
534
protected _getCacheDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI {
535
const npm = descriptor as INpmPluginSource;
536
return joinPath(cacheRoot, 'npm', sanitizeCacheSegment(npm.package));
537
}
538
539
protected _buildInstallArgs(installDir: URI, plugin: IMarketplacePlugin): string[] {
540
const npm = plugin.sourceDescriptor as INpmPluginSource;
541
const packageSpec = npm.version ? `${npm.package}@${npm.version}` : npm.package;
542
const args = ['npm', 'install', '--prefix', installDir.fsPath, packageSpec];
543
if (npm.registry) {
544
args.push('--registry', npm.registry);
545
}
546
return args;
547
}
548
}
549
550
// ---------------------------------------------------------------------------
551
// pip — `{ source: "pip", package: "my-plugin" }`
552
// ---------------------------------------------------------------------------
553
554
export class PipPluginSource extends AbstractPackagePluginSource {
555
readonly kind = PluginSourceKind.Pip;
556
protected readonly _managerName = 'pip';
557
558
getInstallUri(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI {
559
const pip = descriptor as IPipPluginSource;
560
return joinPath(cacheRoot, 'pip', sanitizeCacheSegment(pip.package));
561
}
562
563
getLabel(descriptor: IPluginSourceDescriptor): string {
564
const pip = descriptor as IPipPluginSource;
565
return pip.version ? `${pip.package}==${pip.version}` : pip.package;
566
}
567
568
protected _getCacheDir(cacheRoot: URI, descriptor: IPluginSourceDescriptor): URI {
569
const pip = descriptor as IPipPluginSource;
570
return joinPath(cacheRoot, 'pip', sanitizeCacheSegment(pip.package));
571
}
572
573
protected _buildInstallArgs(installDir: URI, plugin: IMarketplacePlugin): string[] {
574
const pip = plugin.sourceDescriptor as IPipPluginSource;
575
const packageSpec = pip.version ? `${pip.package}==${pip.version}` : pip.package;
576
const args = ['pip', 'install', '--target', installDir.fsPath, packageSpec];
577
if (pip.registry) {
578
args.push('--index-url', pip.registry);
579
}
580
return args;
581
}
582
}
583
584