@@ -231,18 +231,60 @@ function loadMarketplace() {
231231}
232232
233233/**
234- * Resolve the source URL string from a plugin's source field.
235- * Handles both legacy string format ("https://...") and the new object
236- * format ({ source: "url", url: "https://..." }) from Claude Code plugin schema.
237- * Returns null for local/bundled sources or missing values.
234+ * Normalize marketplace plugin source entries.
235+ *
236+ * Supported formats:
237+ * - string URL/path (legacy)
238+ * - object: { source: "url", url: "..." } (current)
239+ * - object: { source: "path", path: "..." } (local/bundled)
240+ *
241+ * @param {string|Object } source
242+ * @returns {{type: 'remote'|'local', value: string}|null }
238243 */
239- function resolveSourceUrl ( source ) {
240- if ( ! source ) return null ;
241- if ( typeof source === 'string' ) return source ;
242- if ( typeof source === 'object' && source . url ) return source . url ;
244+ function resolvePluginSource ( source ) {
245+ if ( typeof source === 'string' ) {
246+ const value = source . trim ( ) ;
247+ if ( ! value ) return null ;
248+ if ( value . startsWith ( './' ) || value . startsWith ( '../' ) ) {
249+ return { type : 'local' , value } ;
250+ }
251+ return { type : 'remote' , value } ;
252+ }
253+
254+ if ( ! source || typeof source !== 'object' ) return null ;
255+
256+ const sourceType = typeof source . source === 'string' ? source . source . toLowerCase ( ) : null ;
257+
258+ if ( ( sourceType === 'path' || sourceType === 'local' ) && typeof source . path === 'string' ) {
259+ return { type : 'local' , value : source . path } ;
260+ }
261+
262+ if ( sourceType === 'url' && typeof source . url === 'string' ) {
263+ return { type : 'remote' , value : source . url } ;
264+ }
265+
266+ // Backward/forward-compatible fallbacks
267+ if ( typeof source . path === 'string' ) {
268+ return { type : 'local' , value : source . path } ;
269+ }
270+ if ( typeof source . url === 'string' ) {
271+ return { type : 'remote' , value : source . url } ;
272+ }
273+
243274 return null ;
244275}
245276
277+ /**
278+ * Backward-compatible helper returning only the source URL/path value.
279+ *
280+ * @param {string|Object } source
281+ * @returns {string|null }
282+ */
283+ function resolveSourceUrl ( source ) {
284+ const normalized = resolvePluginSource ( source ) ;
285+ return normalized ? normalized . value : null ;
286+ }
287+
246288/**
247289 * Resolve plugin dependencies transitively.
248290 *
@@ -313,45 +355,72 @@ async function fetchPlugin(name, source, version) {
313355 }
314356 }
315357
358+ const parsedSource = parseGitHubSource ( source , version , name ) ;
359+ const owner = parsedSource . owner ;
360+ const repo = parsedSource . repo ;
361+
362+ const refCandidates = parsedSource . explicitRef
363+ ? [ parsedSource . ref ]
364+ : [ parsedSource . ref , version , 'main' , 'master' ] ;
365+
366+ let lastError = null ;
367+ for ( const ref of [ ...new Set ( refCandidates . filter ( Boolean ) ) ] ) {
368+ const tarballUrl = `https://api.github.com/repos/${ owner } /${ repo } /tarball/${ ref } ` ;
369+
370+ try {
371+ console . log ( ` Fetching ${ name } @${ version } from ${ owner } /${ repo } (${ ref } )...` ) ;
372+
373+ // Clean and recreate
374+ if ( fs . existsSync ( pluginDir ) ) {
375+ fs . rmSync ( pluginDir , { recursive : true , force : true } ) ;
376+ }
377+ fs . mkdirSync ( pluginDir , { recursive : true } ) ;
378+
379+ // Download and extract tarball
380+ await downloadAndExtractTarball ( tarballUrl , pluginDir ) ;
381+
382+ // Write version marker
383+ fs . writeFileSync ( versionFile , version ) ;
384+ return pluginDir ;
385+ } catch ( err ) {
386+ lastError = err ;
387+ const isNotFound = / H T T P 4 0 4 / . test ( err . message ) ;
388+ if ( isNotFound && ! parsedSource . explicitRef ) {
389+ continue ;
390+ }
391+ throw err ;
392+ }
393+ }
394+
395+ throw new Error (
396+ `Unable to fetch ${ name } from ${ owner } /${ repo } . Tried refs: ${ [ ...new Set ( refCandidates . filter ( Boolean ) ) ] . join ( ', ' ) } . Last error: ${ lastError ? lastError . message : 'unknown error' } `
397+ ) ;
398+ }
399+
400+ /**
401+ * Parse GitHub source URL formats and normalize repo name.
402+ *
403+ * @param {string } source
404+ * @param {string } version
405+ * @param {string } [name]
406+ * @returns {{owner: string, repo: string, ref: string, explicitRef: boolean} }
407+ */
408+ function parseGitHubSource ( source , version , name = 'plugin' ) {
316409 // Parse source formats:
317410 // "https://github.com/owner/repo" or "https://github.com/owner/repo#ref"
318411 // "github:owner/repo" or "github:owner/repo#ref"
319- let owner , repo , ref ;
320412 const urlMatch = source . match ( / g i t h u b \. c o m \/ ( [ ^ / ] + ) \/ ( [ ^ / # ] + ) (?: # ( .+ ) ) ? / ) ;
321413 const shortMatch = ! urlMatch && source . match ( / ^ g i t h u b : ( [ ^ / ] + ) \/ ( [ ^ # ] + ) (?: # ( .+ ) ) ? $ / ) ;
322414 const match = urlMatch || shortMatch ;
323415 if ( ! match ) {
324416 throw new Error ( `Unsupported source format for ${ name } : ${ source } ` ) ;
325417 }
326- owner = match [ 1 ] ;
327- repo = match [ 2 ] . replace ( / \. g i t $ / , '' ) ;
328- ref = match [ 3 ] || `v${ version } ` ;
329-
330- console . log ( ` Fetching ${ name } @${ version } from ${ owner } /${ repo } ...` ) ;
331-
332- // Clean and recreate
333- if ( fs . existsSync ( pluginDir ) ) {
334- fs . rmSync ( pluginDir , { recursive : true , force : true } ) ;
335- }
336- fs . mkdirSync ( pluginDir , { recursive : true } ) ;
337-
338- // Download and extract tarball, falling back to main branch if version tag 404s
339- const tarballUrl = `https://api.github.com/repos/${ owner } /${ repo } /tarball/${ ref } ` ;
340- try {
341- await downloadAndExtractTarball ( tarballUrl , pluginDir ) ;
342- } catch ( err ) {
343- if ( ref !== 'main' && err . message && err . message . includes ( '404' ) ) {
344- const mainUrl = `https://api.github.com/repos/${ owner } /${ repo } /tarball/main` ;
345- await downloadAndExtractTarball ( mainUrl , pluginDir ) ;
346- } else {
347- throw err ;
348- }
349- }
350418
351- // Write version marker
352- fs . writeFileSync ( versionFile , version ) ;
353-
354- return pluginDir ;
419+ const owner = match [ 1 ] ;
420+ const repo = match [ 2 ] . replace ( / \. g i t $ / , '' ) ;
421+ const explicitRef = Boolean ( match [ 3 ] ) ;
422+ const ref = match [ 3 ] || `v${ version } ` ;
423+ return { owner, repo, ref, explicitRef } ;
355424}
356425
357426/**
@@ -470,16 +539,17 @@ async function fetchExternalPlugins(pluginNames, marketplace) {
470539 const plugin = pluginMap [ name ] ;
471540 if ( ! plugin ) continue ;
472541
473- // If source is local (starts with ./), plugin is bundled - just use PACKAGE_DIR
474- const sourceUrl = resolveSourceUrl ( plugin . source ) ;
475- if ( ! sourceUrl || sourceUrl . startsWith ( './' ) || sourceUrl . startsWith ( '../' ) ) {
542+ const source = resolvePluginSource ( plugin . source ) ;
543+
544+ // Local/bundled plugin, no external fetch needed
545+ if ( ! source || source . type === 'local' ) {
476546 // Bundled plugin, no fetch needed
477547 fetched . push ( name ) ;
478548 continue ;
479549 }
480550
481551 try {
482- await fetchPlugin ( name , sourceUrl , plugin . version ) ;
552+ await fetchPlugin ( name , source . value , plugin . version ) ;
483553 fetched . push ( name ) ;
484554 } catch ( err ) {
485555 failed . push ( name ) ;
@@ -493,6 +563,8 @@ async function fetchExternalPlugins(pluginNames, marketplace) {
493563 console . error ( `\n [WARN] Missing dependencies: ${ missingDeps . join ( ', ' ) } ` ) ;
494564 console . error ( ` Some plugins may not work correctly without their dependencies.` ) ;
495565 }
566+
567+ throw new Error ( `Failed to fetch ${ failed . length } plugin(s): ${ failed . join ( ', ' ) } ` ) ;
496568 }
497569
498570 return fetched ;
@@ -917,12 +989,15 @@ async function installPlugin(nameWithVersion, args) {
917989 // Fetch all
918990 for ( const depName of toFetch ) {
919991 const dep = pluginMap [ depName ] ;
920- const depSourceUrl = resolveSourceUrl ( dep && dep . source ) ;
921- if ( ! dep || ! depSourceUrl || depSourceUrl . startsWith ( './' ) ) continue ;
992+ if ( ! dep ) continue ;
993+
994+ const source = resolvePluginSource ( dep . source ) ;
995+ if ( ! source || source . type === 'local' ) continue ;
996+
922997 checkCoreCompat ( dep ) ;
923998 const ver = depName === name && requestedVersion ? requestedVersion : dep . version ;
924999 try {
925- await fetchPlugin ( depName , depSourceUrl , ver ) ;
1000+ await fetchPlugin ( depName , source . value , ver ) ;
9261001 } catch ( err ) {
9271002 console . error ( ` [ERROR] Failed to fetch ${ depName } : ${ err . message } ` ) ;
9281003 }
@@ -1931,8 +2006,6 @@ async function main() {
19312006 if ( entry ) checkCoreCompat ( entry ) ;
19322007 }
19332008
1934- await fetchExternalPlugins ( pluginNames , marketplace ) ;
1935-
19362009 // Only copy to ~/.agentsys if OpenCode, Codex, or Cursor selected (they need local files)
19372010 const needsLocalInstall = selected . includes ( 'opencode' ) || selected . includes ( 'codex' ) || selected . includes ( 'cursor' ) ;
19382011 let installDir = null ;
@@ -1944,6 +2017,8 @@ async function main() {
19442017 installDependencies ( installDir ) ;
19452018 }
19462019
2020+ await fetchExternalPlugins ( pluginNames , marketplace ) ;
2021+
19472022 // Install for each platform
19482023 const failedPlatforms = [ ] ;
19492024 for ( const platform of selected ) {
@@ -2023,5 +2098,7 @@ module.exports = {
20232098 loadComponents,
20242099 resolveComponent,
20252100 buildFilterFromComponent,
2101+ resolvePluginSource,
2102+ parseGitHubSource,
20262103 installForCursor
20272104} ;
0 commit comments