1414 BUILTIN_PLUGIN_REPOS ,
1515 VALID_APP_TYPES ,
1616 PluginManager ,
17+ PluginRepo ,
18+ fetch_repo_info_from_url ,
19+ parse_github_url ,
1720)
1821from code_assistant_manager .plugins .claude import ClaudePluginHandler
1922
@@ -165,7 +168,7 @@ def marketplace_update(
165168def install_plugin (
166169 plugin : str = typer .Argument (
167170 ...,
168- help = "Plugin name or plugin@marketplace" ,
171+ help = "Plugin name, plugin@marketplace, or marketplace name " ,
169172 ),
170173 marketplace : Optional [str ] = typer .Option (
171174 None ,
@@ -174,26 +177,54 @@ def install_plugin(
174177 help = "Marketplace name (alternative to plugin@marketplace format)" ,
175178 ),
176179):
177- """Install a plugin from available marketplaces."""
180+ """Install a plugin from available marketplaces or add a built-in marketplace ."""
178181 _check_claude_cli ()
179182 handler = _get_handler ()
183+ manager = PluginManager ()
180184
181- # Check if it's a built-in repo that needs marketplace added first
182- builtin = BUILTIN_PLUGIN_REPOS .get (plugin )
183- if builtin and builtin .repo_owner and builtin .repo_name :
184- # Check if marketplace already exists
185+ # Check if it's a configured repo (user repos take precedence over builtin)
186+ configured_repo = manager .get_repo (plugin )
187+ if configured_repo and configured_repo .repo_owner and configured_repo .repo_name :
188+ repo_url = f"https://github.com/{ configured_repo .repo_owner } /{ configured_repo .repo_name } "
189+
190+ # Handle marketplace type - just add the marketplace
191+ if configured_repo .type == "marketplace" :
192+ typer .echo (f"{ Colors .CYAN } Adding marketplace: { plugin } ...{ Colors .RESET } " )
193+ success , msg = handler .marketplace_add (repo_url )
194+ if success :
195+ typer .echo (f"{ Colors .GREEN } ✓ Marketplace added: { plugin } { Colors .RESET } " )
196+ typer .echo (
197+ f"\n { Colors .CYAN } Browse plugins with:{ Colors .RESET } cam plugin search --marketplace { plugin } "
198+ )
199+ typer .echo (
200+ f"{ Colors .CYAN } Install plugins with:{ Colors .RESET } cam plugin install <plugin-name>@{ plugin } "
201+ )
202+ elif "already installed" in msg .lower ():
203+ typer .echo (
204+ f"{ Colors .YELLOW } Marketplace '{ plugin } ' is already installed.{ Colors .RESET } "
205+ )
206+ typer .echo (
207+ f"\n { Colors .CYAN } Browse plugins with:{ Colors .RESET } cam plugin search --marketplace { plugin } "
208+ )
209+ else :
210+ typer .echo (
211+ f"{ Colors .RED } ✗ Failed to add marketplace: { msg } { Colors .RESET } "
212+ )
213+ raise typer .Exit (1 )
214+ return
215+
216+ # Handle plugin type - add marketplace first if needed, then install plugin
185217 known_marketplaces = handler .get_known_marketplaces ()
186218 marketplace_exists = any (
187- builtin .repo_owner .lower () in name .lower ()
188- or builtin .repo_name .lower () in name .lower ()
219+ configured_repo .repo_owner .lower () in name .lower ()
220+ or configured_repo .repo_name .lower () in name .lower ()
189221 for name in known_marketplaces
190222 )
191223
192224 if not marketplace_exists :
193225 typer .echo (
194- f"{ Colors .CYAN } Adding marketplace for built-in plugin: { plugin } ...{ Colors .RESET } "
226+ f"{ Colors .CYAN } Adding marketplace for plugin: { plugin } ...{ Colors .RESET } "
195227 )
196- repo_url = f"https://github.com/{ builtin .repo_owner } /{ builtin .repo_name } "
197228 success , msg = handler .marketplace_add (repo_url )
198229 if not success and "already installed" not in msg .lower ():
199230 typer .echo (
@@ -377,21 +408,32 @@ def list_plugins(
377408
378409@plugin_app .command ("repos" )
379410def list_repos ():
380- """List available built-in plugin repositories."""
381- if not BUILTIN_PLUGIN_REPOS :
411+ """List available plugin repositories and marketplaces (built-in + user)."""
412+ manager = PluginManager ()
413+
414+ # Get all repos (builtin + user)
415+ all_repos = manager .get_all_repos ()
416+ user_repos = manager .get_user_repos ()
417+
418+ if not all_repos :
419+ typer .echo (f"{ Colors .YELLOW } No plugin repositories available.{ Colors .RESET } " )
382420 typer .echo (
383- f"{ Colors .YELLOW } No built-in plugin repositories available. { Colors .RESET } "
421+ f"\n { Colors .CYAN } Add a repo with: { Colors .RESET } cam plugin fetch <github-url> --save "
384422 )
385423 return
386424
387- typer .echo (f"\n { Colors .BOLD } Built-in Plugin Repositories:{ Colors .RESET } \n " )
388- for name , repo in sorted (BUILTIN_PLUGIN_REPOS .items ()):
425+ # Separate plugins and marketplaces
426+ plugins = {k : v for k , v in all_repos .items () if v .type == "plugin" }
427+ marketplaces = {k : v for k , v in all_repos .items () if v .type == "marketplace" }
428+
429+ def _print_repo (name : str , repo : PluginRepo , is_user : bool = False ):
389430 status = (
390431 f"{ Colors .GREEN } ✓{ Colors .RESET } "
391432 if repo .enabled
392433 else f"{ Colors .RED } ✗{ Colors .RESET } "
393434 )
394- typer .echo (f"{ status } { Colors .BOLD } { name } { Colors .RESET } " )
435+ user_tag = f" { Colors .YELLOW } (user){ Colors .RESET } " if is_user else ""
436+ typer .echo (f"{ status } { Colors .BOLD } { name } { Colors .RESET } { user_tag } " )
395437 if repo .description :
396438 typer .echo (f" { Colors .CYAN } Description:{ Colors .RESET } { repo .description } " )
397439 if repo .repo_owner and repo .repo_name :
@@ -400,7 +442,274 @@ def list_repos():
400442 )
401443 typer .echo ()
402444
403- typer .echo (f"{ Colors .CYAN } Install with:{ Colors .RESET } cam plugin install <name>" )
445+ if plugins :
446+ typer .echo (f"\n { Colors .BOLD } Plugins:{ Colors .RESET } \n " )
447+ for name , repo in sorted (plugins .items ()):
448+ _print_repo (name , repo , name in user_repos )
449+ typer .echo (
450+ f"{ Colors .CYAN } Install with:{ Colors .RESET } cam plugin install <name>"
451+ )
452+
453+ if marketplaces :
454+ typer .echo (f"\n { Colors .BOLD } Marketplaces:{ Colors .RESET } \n " )
455+ for name , repo in sorted (marketplaces .items ()):
456+ _print_repo (name , repo , name in user_repos )
457+ typer .echo (
458+ f"{ Colors .CYAN } Add marketplace with:{ Colors .RESET } cam plugin install <marketplace-name>"
459+ )
460+
461+ typer .echo (
462+ f"\n { Colors .CYAN } Add new repo:{ Colors .RESET } cam plugin fetch <github-url> --save"
463+ )
464+ typer .echo ()
465+
466+
467+ @plugin_app .command ("browse" )
468+ def browse_marketplace (
469+ marketplace : str = typer .Argument (
470+ ...,
471+ help = "Marketplace name to browse (from 'cam plugin repos')" ,
472+ ),
473+ query : Optional [str ] = typer .Option (
474+ None ,
475+ "--query" ,
476+ "-q" ,
477+ help = "Filter plugins by name or description" ,
478+ ),
479+ category : Optional [str ] = typer .Option (
480+ None ,
481+ "--category" ,
482+ "-c" ,
483+ help = "Filter plugins by category" ,
484+ ),
485+ limit : int = typer .Option (
486+ 50 ,
487+ "--limit" ,
488+ "-n" ,
489+ help = "Maximum number of plugins to show" ,
490+ ),
491+ ):
492+ """Browse plugins in a configured marketplace.
493+
494+ Fetches the marketplace manifest from GitHub and lists all available plugins.
495+ Use --query to search by name/description, --category to filter by category.
496+ """
497+ manager = PluginManager ()
498+
499+ # Get the marketplace repo config
500+ repo = manager .get_repo (marketplace )
501+ if not repo :
502+ typer .echo (
503+ f"{ Colors .RED } ✗ Marketplace '{ marketplace } ' not found.{ Colors .RESET } "
504+ )
505+ typer .echo (f"\n { Colors .CYAN } Available repos:{ Colors .RESET } " )
506+ for name in manager .get_all_repos ():
507+ typer .echo (f" • { name } " )
508+ raise typer .Exit (1 )
509+
510+ if repo .type != "marketplace" :
511+ typer .echo (
512+ f"{ Colors .RED } ✗ '{ marketplace } ' is a plugin, not a marketplace.{ Colors .RESET } "
513+ )
514+ typer .echo (
515+ f"\n { Colors .CYAN } To install:{ Colors .RESET } cam plugin install { marketplace } "
516+ )
517+ raise typer .Exit (1 )
518+
519+ typer .echo (f"{ Colors .CYAN } Fetching plugins from { marketplace } ...{ Colors .RESET } " )
520+
521+ # Fetch marketplace info
522+ if not repo .repo_owner or not repo .repo_name :
523+ typer .echo (f"{ Colors .RED } ✗ Marketplace missing repo info.{ Colors .RESET } " )
524+ raise typer .Exit (1 )
525+
526+ from code_assistant_manager .plugins .fetch import fetch_repo_info
527+
528+ info = fetch_repo_info (repo .repo_owner , repo .repo_name , repo .repo_branch )
529+ if not info or not info .plugins :
530+ typer .echo (f"{ Colors .RED } ✗ Could not fetch marketplace plugins.{ Colors .RESET } " )
531+ raise typer .Exit (1 )
532+
533+ # Filter plugins
534+ plugins = info .plugins
535+ if query :
536+ query_lower = query .lower ()
537+ plugins = [
538+ p
539+ for p in plugins
540+ if query_lower in p .get ("name" , "" ).lower ()
541+ or query_lower in p .get ("description" , "" ).lower ()
542+ ]
543+
544+ if category :
545+ category_lower = category .lower ()
546+ plugins = [
547+ p for p in plugins if category_lower in p .get ("category" , "" ).lower ()
548+ ]
549+
550+ # Display results
551+ total = len (plugins )
552+ plugins = plugins [:limit ]
553+
554+ typer .echo (
555+ f"\n { Colors .BOLD } { info .name } { Colors .RESET } - { info .description or 'No description' } "
556+ )
557+ if info .version :
558+ typer .echo (f"Version: { info .version } " )
559+ typer .echo (f"Total plugins: { info .plugin_count } " )
560+
561+ if query or category :
562+ typer .echo (f"Matching: { total } " )
563+
564+ typer .echo (f"\n { Colors .BOLD } Plugins:{ Colors .RESET } \n " )
565+
566+ # Get unique categories for reference
567+ categories = set ()
568+ for p in info .plugins :
569+ if p .get ("category" ):
570+ categories .add (p ["category" ])
571+
572+ for p in plugins :
573+ name = p .get ("name" , "unknown" )
574+ version = p .get ("version" , "" )
575+ desc = p .get ("description" , "" )
576+ cat = p .get ("category" , "" )
577+
578+ version_str = f" v{ version } " if version else ""
579+ cat_str = f" [{ cat } ]" if cat else ""
580+
581+ typer .echo (
582+ f" { Colors .BOLD } { name } { Colors .RESET } { version_str } { Colors .CYAN } { cat_str } { Colors .RESET } "
583+ )
584+ if desc :
585+ # Truncate long descriptions
586+ if len (desc ) > 80 :
587+ desc = desc [:77 ] + "..."
588+ typer .echo (f" { desc } " )
589+
590+ if total > limit :
591+ typer .echo (f"\n ... and { total - limit } more" )
592+
593+ # Show categories if available
594+ if categories :
595+ typer .echo (
596+ f"\n { Colors .CYAN } Categories:{ Colors .RESET } { ', ' .join (sorted (categories ))} "
597+ )
598+
599+ typer .echo (
600+ f"\n { Colors .CYAN } Install with:{ Colors .RESET } cam plugin install <plugin-name>@{ marketplace } "
601+ )
602+ typer .echo ()
603+
604+
605+ @plugin_app .command ("fetch" )
606+ def fetch_repo (
607+ url : str = typer .Argument (
608+ ...,
609+ help = "GitHub URL or owner/repo (e.g., https://github.com/owner/repo or owner/repo)" ,
610+ ),
611+ save : bool = typer .Option (
612+ False ,
613+ "--save" ,
614+ "-s" ,
615+ help = "Save the fetched repo to user config" ,
616+ ),
617+ ):
618+ """Fetch and detect repo type (plugin or marketplace) from GitHub.
619+
620+ Analyzes a GitHub repository to determine if it's a single plugin
621+ or a marketplace with multiple plugins, then optionally saves it
622+ to your local configuration.
623+ """
624+ typer .echo (f"{ Colors .CYAN } Fetching repository info...{ Colors .RESET } " )
625+
626+ # Parse and validate URL
627+ parsed = parse_github_url (url )
628+ if not parsed :
629+ typer .echo (f"{ Colors .RED } ✗ Invalid GitHub URL: { url } { Colors .RESET } " )
630+ raise typer .Exit (1 )
631+
632+ owner , repo , branch = parsed
633+ typer .echo (f" Repository: { Colors .BOLD } { owner } /{ repo } { Colors .RESET } " )
634+
635+ # Fetch repo info
636+ info = fetch_repo_info_from_url (url )
637+ if not info :
638+ typer .echo (
639+ f"{ Colors .RED } ✗ Could not fetch repository info. "
640+ f"Make sure the repo has .claude-plugin/marketplace.json{ Colors .RESET } "
641+ )
642+ raise typer .Exit (1 )
643+
644+ # Display results
645+ typer .echo (f"\n { Colors .BOLD } Repository Information:{ Colors .RESET } \n " )
646+ typer .echo (f" { Colors .CYAN } Name:{ Colors .RESET } { info .name } " )
647+ typer .echo (f" { Colors .CYAN } Type:{ Colors .RESET } { info .type } " )
648+ typer .echo (f" { Colors .CYAN } Description:{ Colors .RESET } { info .description or 'N/A' } " )
649+ typer .echo (f" { Colors .CYAN } Branch:{ Colors .RESET } { info .branch } " )
650+
651+ if info .version :
652+ typer .echo (f" { Colors .CYAN } Version:{ Colors .RESET } { info .version } " )
653+
654+ if info .type == "marketplace" :
655+ typer .echo (f" { Colors .CYAN } Plugin Count:{ Colors .RESET } { info .plugin_count } " )
656+ if info .plugins and len (info .plugins ) <= 10 :
657+ typer .echo (f"\n { Colors .CYAN } Plugins:{ Colors .RESET } " )
658+ for p in info .plugins [:10 ]:
659+ typer .echo (f" • { p .get ('name' , 'unknown' )} " )
660+ elif info .plugins :
661+ typer .echo (f"\n { Colors .CYAN } Plugins:{ Colors .RESET } (showing first 10)" )
662+ for p in info .plugins [:10 ]:
663+ typer .echo (f" • { p .get ('name' , 'unknown' )} " )
664+ typer .echo (f" ... and { len (info .plugins ) - 10 } more" )
665+ else :
666+ if info .plugin_path :
667+ typer .echo (f" { Colors .CYAN } Plugin Path:{ Colors .RESET } { info .plugin_path } " )
668+
669+ # Save if requested
670+ if save :
671+ manager = PluginManager ()
672+
673+ # Check if already exists
674+ existing = manager .get_repo (info .name )
675+ if existing :
676+ typer .echo (
677+ f"\n { Colors .YELLOW } Repository '{ info .name } ' already exists in config.{ Colors .RESET } "
678+ )
679+ if not typer .confirm ("Overwrite?" ):
680+ raise typer .Exit (0 )
681+
682+ # Create PluginRepo and save
683+ plugin_repo = PluginRepo (
684+ name = info .name ,
685+ description = info .description ,
686+ repo_owner = info .owner ,
687+ repo_name = info .repo ,
688+ repo_branch = info .branch ,
689+ plugin_path = info .plugin_path ,
690+ type = info .type ,
691+ enabled = True ,
692+ )
693+ manager .add_user_repo (plugin_repo )
694+ typer .echo (
695+ f"\n { Colors .GREEN } ✓ Saved '{ info .name } ' to user config as { info .type } { Colors .RESET } "
696+ )
697+ typer .echo (f" Config file: { manager .plugin_repos_file } " )
698+
699+ # Show next steps
700+ if info .type == "marketplace" :
701+ typer .echo (
702+ f"\n { Colors .CYAN } Next:{ Colors .RESET } cam plugin install { info .name } "
703+ )
704+ else :
705+ typer .echo (
706+ f"\n { Colors .CYAN } Next:{ Colors .RESET } cam plugin install { info .name } "
707+ )
708+ else :
709+ typer .echo (
710+ f"\n { Colors .CYAN } To save:{ Colors .RESET } cam plugin fetch '{ url } ' --save"
711+ )
712+
404713 typer .echo ()
405714
406715
0 commit comments