@@ -18,7 +18,7 @@ import { IProgressService, ProgressLocation } from '../../../../platform/progres
1818import { IStorageService , StorageScope , StorageTarget } from '../../../../platform/storage/common/storage.js' ;
1919import type { Dto } from '../../../services/extensions/common/proxyIdentifier.js' ;
2020import { IAgentPluginRepositoryService , IEnsureRepositoryOptions , IPullRepositoryOptions } from '../common/plugins/agentPluginRepositoryService.js' ;
21- import { IMarketplacePlugin , IMarketplaceReference , MarketplaceReferenceKind , MarketplaceType } from '../common/plugins/pluginMarketplaceService.js' ;
21+ import { IMarketplacePlugin , IMarketplaceReference , IPluginSourceDescriptor , MarketplaceReferenceKind , MarketplaceType , PluginSourceKind } from '../common/plugins/pluginMarketplaceService.js' ;
2222
2323const MARKETPLACE_INDEX_STORAGE_KEY = 'chat.plugins.marketplaces.index.v1' ;
2424
@@ -176,7 +176,7 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi
176176 this . _storageService . store ( MARKETPLACE_INDEX_STORAGE_KEY , JSON . stringify ( serialized ) , StorageScope . APPLICATION , StorageTarget . MACHINE ) ;
177177 }
178178
179- private async _cloneRepository ( repoDir : URI , cloneUrl : string , progressTitle : string , failureLabel : string ) : Promise < void > {
179+ private async _cloneRepository ( repoDir : URI , cloneUrl : string , progressTitle : string , failureLabel : string , ref ?: string ) : Promise < void > {
180180 try {
181181 await this . _progressService . withProgress (
182182 {
@@ -186,7 +186,7 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi
186186 } ,
187187 async ( ) => {
188188 await this . _fileService . createFolder ( dirname ( repoDir ) ) ;
189- await this . _commandService . executeCommand ( '_git.cloneRepository' , cloneUrl , dirname ( repoDir ) . fsPath ) ;
189+ await this . _commandService . executeCommand ( '_git.cloneRepository' , cloneUrl , repoDir . fsPath ) ;
190190 }
191191 ) ;
192192 } catch ( err ) {
@@ -212,4 +212,178 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi
212212 }
213213 return pluginDir ;
214214 }
215+
216+ getPluginSourceInstallUri ( sourceDescriptor : IPluginSourceDescriptor ) : URI {
217+ switch ( sourceDescriptor . kind ) {
218+ case PluginSourceKind . RelativePath :
219+ throw new Error ( 'Use getPluginInstallUri() for relative-path sources' ) ;
220+ case PluginSourceKind . GitHub : {
221+ const [ owner , repo ] = sourceDescriptor . repo . split ( '/' ) ;
222+ return joinPath ( this . _cacheRoot , 'github.com' , owner , repo , ...this . _getSourceRevisionCacheSuffix ( sourceDescriptor ) ) ;
223+ }
224+ case PluginSourceKind . GitUrl : {
225+ const segments = this . _gitUrlCacheSegments ( sourceDescriptor . url , sourceDescriptor . ref , sourceDescriptor . sha ) ;
226+ return joinPath ( this . _cacheRoot , ...segments ) ;
227+ }
228+ case PluginSourceKind . Npm :
229+ return joinPath ( this . _cacheRoot , 'npm' , sanitizePackageName ( sourceDescriptor . package ) , 'node_modules' , sourceDescriptor . package ) ;
230+ case PluginSourceKind . Pip :
231+ return joinPath ( this . _cacheRoot , 'pip' , sanitizePackageName ( sourceDescriptor . package ) ) ;
232+ }
233+ }
234+
235+ async ensurePluginSource ( plugin : IMarketplacePlugin , options ?: IEnsureRepositoryOptions ) : Promise < URI > {
236+ const descriptor = plugin . sourceDescriptor ;
237+ switch ( descriptor . kind ) {
238+ case PluginSourceKind . RelativePath :
239+ return this . ensureRepository ( plugin . marketplaceReference , options ) ;
240+ case PluginSourceKind . GitHub : {
241+ const cloneUrl = `https://github.com/${ descriptor . repo } .git` ;
242+ const repoDir = this . getPluginSourceInstallUri ( descriptor ) ;
243+ const repoExists = await this . _fileService . exists ( repoDir ) ;
244+ if ( repoExists ) {
245+ await this . _checkoutPluginSourceRevision ( repoDir , descriptor , options ?. failureLabel ?? descriptor . repo ) ;
246+ return repoDir ;
247+ }
248+ const progressTitle = options ?. progressTitle ?? localize ( 'cloningPluginSource' , "Cloning plugin source '{0}'..." , descriptor . repo ) ;
249+ const failureLabel = options ?. failureLabel ?? descriptor . repo ;
250+ await this . _cloneRepository ( repoDir , cloneUrl , progressTitle , failureLabel , descriptor . ref ) ;
251+ await this . _checkoutPluginSourceRevision ( repoDir , descriptor , failureLabel ) ;
252+ return repoDir ;
253+ }
254+ case PluginSourceKind . GitUrl : {
255+ const repoDir = this . getPluginSourceInstallUri ( descriptor ) ;
256+ const repoExists = await this . _fileService . exists ( repoDir ) ;
257+ if ( repoExists ) {
258+ await this . _checkoutPluginSourceRevision ( repoDir , descriptor , options ?. failureLabel ?? descriptor . url ) ;
259+ return repoDir ;
260+ }
261+ const progressTitle = options ?. progressTitle ?? localize ( 'cloningPluginSourceUrl' , "Cloning plugin source '{0}'..." , descriptor . url ) ;
262+ const failureLabel = options ?. failureLabel ?? descriptor . url ;
263+ await this . _cloneRepository ( repoDir , descriptor . url , progressTitle , failureLabel , descriptor . ref ) ;
264+ await this . _checkoutPluginSourceRevision ( repoDir , descriptor , failureLabel ) ;
265+ return repoDir ;
266+ }
267+ case PluginSourceKind . Npm : {
268+ // npm/pip install directories are managed by the install service.
269+ // Return the expected install URI without performing installation.
270+ return joinPath ( this . _cacheRoot , 'npm' , sanitizePackageName ( descriptor . package ) ) ;
271+ }
272+ case PluginSourceKind . Pip : {
273+ return joinPath ( this . _cacheRoot , 'pip' , sanitizePackageName ( descriptor . package ) ) ;
274+ }
275+ }
276+ }
277+
278+ async updatePluginSource ( plugin : IMarketplacePlugin , options ?: IPullRepositoryOptions ) : Promise < void > {
279+ const descriptor = plugin . sourceDescriptor ;
280+ if ( descriptor . kind !== PluginSourceKind . GitHub && descriptor . kind !== PluginSourceKind . GitUrl ) {
281+ return ;
282+ }
283+
284+ const repoDir = this . getPluginSourceInstallUri ( descriptor ) ;
285+ const repoExists = await this . _fileService . exists ( repoDir ) ;
286+ if ( ! repoExists ) {
287+ this . _logService . warn ( `[AgentPluginRepositoryService] Cannot update plugin '${ options ?. pluginName ?? plugin . name } ': source repository not cloned` ) ;
288+ return ;
289+ }
290+
291+ const updateLabel = options ?. pluginName ?? plugin . name ;
292+ const failureLabel = options ?. failureLabel ?? updateLabel ;
293+
294+ try {
295+ await this . _progressService . withProgress (
296+ {
297+ location : ProgressLocation . Notification ,
298+ title : localize ( 'updatingPluginSource' , "Updating plugin '{0}'..." , updateLabel ) ,
299+ cancellable : false ,
300+ } ,
301+ async ( ) => {
302+ await this . _commandService . executeCommand ( 'git.openRepository' , repoDir . fsPath ) ;
303+ if ( descriptor . sha ) {
304+ await this . _commandService . executeCommand ( 'git.fetch' , repoDir . fsPath ) ;
305+ } else {
306+ await this . _commandService . executeCommand ( '_git.pull' , repoDir . fsPath ) ;
307+ }
308+ await this . _checkoutPluginSourceRevision ( repoDir , descriptor , failureLabel ) ;
309+ }
310+ ) ;
311+ } catch ( err ) {
312+ this . _logService . error ( `[AgentPluginRepositoryService] Failed to update plugin source ${ updateLabel } :` , err ) ;
313+ this . _notificationService . notify ( {
314+ severity : Severity . Error ,
315+ message : localize ( 'pullPluginSourceFailed' , "Failed to update plugin '{0}': {1}" , failureLabel , err ?. message ?? String ( err ) ) ,
316+ actions : {
317+ primary : [ new Action ( 'showGitOutput' , localize ( 'showGitOutput' , "Show Git Output" ) , undefined , true , ( ) => {
318+ this . _commandService . executeCommand ( 'git.showOutput' ) ;
319+ } ) ] ,
320+ } ,
321+ } ) ;
322+ }
323+ }
324+
325+ private _gitUrlCacheSegments ( url : string , ref ?: string , sha ?: string ) : string [ ] {
326+ try {
327+ const parsed = URI . parse ( url ) ;
328+ const authority = ( parsed . authority || 'unknown' ) . replace ( / [ \\ / : * ? " < > | ] / g, '_' ) . toLowerCase ( ) ;
329+ const pathPart = parsed . path . replace ( / ^ \/ + / , '' ) . replace ( / \. g i t $ / i, '' ) . replace ( / \/ + $ / g, '' ) ;
330+ const segments = pathPart . split ( '/' ) . map ( s => s . replace ( / [ \\ / : * ? " < > | ] / g, '_' ) ) ;
331+ return [ authority , ...segments , ...this . _getSourceRevisionCacheSuffix ( ref , sha ) ] ;
332+ } catch {
333+ return [ 'git' , url . replace ( / [ \\ / : * ? " < > | ] / g, '_' ) , ...this . _getSourceRevisionCacheSuffix ( ref , sha ) ] ;
334+ }
335+ }
336+
337+ private _getSourceRevisionCacheSuffix ( descriptorOrRef : IPluginSourceDescriptor | string | undefined , sha ?: string ) : string [ ] {
338+ if ( typeof descriptorOrRef === 'object' && descriptorOrRef ) {
339+ if ( descriptorOrRef . kind === PluginSourceKind . GitHub || descriptorOrRef . kind === PluginSourceKind . GitUrl ) {
340+ return this . _getSourceRevisionCacheSuffix ( descriptorOrRef . ref , descriptorOrRef . sha ) ;
341+ }
342+ return [ ] ;
343+ }
344+
345+ const ref = descriptorOrRef ;
346+ if ( sha ) {
347+ return [ `sha_${ sanitizePackageName ( sha ) } ` ] ;
348+ }
349+ if ( ref ) {
350+ return [ `ref_${ sanitizePackageName ( ref ) } ` ] ;
351+ }
352+ return [ ] ;
353+ }
354+
355+ private async _checkoutPluginSourceRevision ( repoDir : URI , descriptor : IPluginSourceDescriptor , failureLabel : string ) : Promise < void > {
356+ if ( descriptor . kind !== PluginSourceKind . GitHub && descriptor . kind !== PluginSourceKind . GitUrl ) {
357+ return ;
358+ }
359+
360+ if ( ! descriptor . sha && ! descriptor . ref ) {
361+ return ;
362+ }
363+
364+ try {
365+ if ( descriptor . sha ) {
366+ await this . _commandService . executeCommand ( '_git.checkout' , repoDir . fsPath , descriptor . sha , true ) ;
367+ return ;
368+ }
369+
370+ await this . _commandService . executeCommand ( '_git.checkout' , repoDir . fsPath , descriptor . ref ) ;
371+ } catch ( err ) {
372+ this . _logService . error ( `[AgentPluginRepositoryService] Failed to checkout plugin source revision for ${ failureLabel } :` , err ) ;
373+ this . _notificationService . notify ( {
374+ severity : Severity . Error ,
375+ message : localize ( 'checkoutPluginSourceFailed' , "Failed to checkout plugin '{0}' to requested revision: {1}" , failureLabel , err ?. message ?? String ( err ) ) ,
376+ actions : {
377+ primary : [ new Action ( 'showGitOutput' , localize ( 'showGitOutput' , "Show Git Output" ) , undefined , true , ( ) => {
378+ this . _commandService . executeCommand ( 'git.showOutput' ) ;
379+ } ) ] ,
380+ } ,
381+ } ) ;
382+ throw err ;
383+ }
384+ }
385+ }
386+
387+ function sanitizePackageName ( name : string ) : string {
388+ return name . replace ( / [ \\ / : * ? " < > | ] / g, '_' ) ;
215389}
0 commit comments