1010 CustomizationType ,
1111 PluginInfo ,
1212)
13+ from lazyclaude .models .marketplace import MarketplacePlugin
1314from lazyclaude .services .filesystem_scanner import (
1415 FilesystemScanner ,
1516 GlobStrategy ,
@@ -97,14 +98,19 @@ def refresh(self) -> list[Customization]:
9798
9899 @abstractmethod
99100 def discover_from_directory (
100- self , plugin_dir : Path , plugin_info : PluginInfo | None = None
101+ self ,
102+ plugin_dir : Path ,
103+ plugin_info : PluginInfo | None = None ,
104+ marketplace_plugin : MarketplacePlugin | None = None ,
101105 ) -> list [Customization ]:
102106 """
103107 Discover customizations from a specific directory (for plugin preview).
104108
105109 Args:
106110 plugin_dir: The directory to scan for customizations.
107111 plugin_info: Optional plugin info to attach to customizations.
112+ marketplace_plugin: Optional marketplace plugin with extra metadata
113+ containing skill paths etc.
108114
109115 Returns:
110116 List of customizations found in the directory.
@@ -194,7 +200,10 @@ def get_active_config_path(self) -> Path:
194200 return self .user_config_path
195201
196202 def discover_from_directory (
197- self , plugin_dir : Path , plugin_info : PluginInfo | None = None
203+ self ,
204+ plugin_dir : Path ,
205+ plugin_info : PluginInfo | None = None ,
206+ marketplace_plugin : MarketplacePlugin | None = None ,
198207 ) -> list [Customization ]:
199208 """Discover customizations from a specific directory (for plugin preview)."""
200209 customizations : list [Customization ] = []
@@ -208,6 +217,14 @@ def discover_from_directory(
208217 plugin_scanner .scan_directory (plugin_dir , config , level , plugin_info )
209218 )
210219
220+ if marketplace_plugin :
221+ seen_paths = {c .path .resolve () for c in customizations if c .path }
222+ customizations .extend (
223+ self ._discover_marketplace_components (
224+ plugin_dir , marketplace_plugin , plugin_info , seen_paths
225+ )
226+ )
227+
211228 if plugin_info :
212229 customizations .extend (self ._discover_plugin_mcps (plugin_dir , plugin_info ))
213230 customizations .extend (self ._discover_plugin_hooks (plugin_dir , plugin_info ))
@@ -224,6 +241,168 @@ def _sort_customizations(
224241 key = lambda c : (type_order [c .type ], c .name .lower ()),
225242 )
226243
244+ def _discover_marketplace_components (
245+ self ,
246+ plugin_dir : Path ,
247+ marketplace_plugin : MarketplacePlugin ,
248+ plugin_info : PluginInfo | None ,
249+ seen_paths : set [Path ],
250+ ) -> list [Customization ]:
251+ """Discover components using custom paths from marketplace.json."""
252+ customizations : list [Customization ] = []
253+ extra = marketplace_plugin .extra_metadata
254+
255+ commands_paths = self ._normalize_paths (extra .get ("commands" ))
256+ if commands_paths :
257+ cmd_parser = SlashCommandParser (plugin_dir )
258+ customizations .extend (
259+ self ._discover_md_files_from_paths (
260+ cmd_parser , plugin_dir , commands_paths , plugin_info , seen_paths
261+ )
262+ )
263+
264+ agents_paths = self ._normalize_paths (extra .get ("agents" ))
265+ if agents_paths :
266+ agent_parser = SubagentParser (plugin_dir )
267+ customizations .extend (
268+ self ._discover_md_files_from_paths (
269+ agent_parser , plugin_dir , agents_paths , plugin_info , seen_paths
270+ )
271+ )
272+
273+ skills_paths = self ._normalize_paths (extra .get ("skills" ))
274+ if skills_paths :
275+ customizations .extend (
276+ self ._discover_custom_skills (
277+ plugin_dir , skills_paths , plugin_info , seen_paths
278+ )
279+ )
280+
281+ mcp_servers = extra .get ("mcpServers" )
282+ if mcp_servers and isinstance (mcp_servers , str ):
283+ customizations .extend (
284+ self ._discover_custom_mcps (plugin_dir , mcp_servers , plugin_info )
285+ )
286+
287+ hooks = extra .get ("hooks" )
288+ if hooks and isinstance (hooks , str ):
289+ customizations .extend (
290+ self ._discover_custom_hooks (plugin_dir , hooks , plugin_info )
291+ )
292+
293+ return customizations
294+
295+ @staticmethod
296+ def _normalize_paths (value : str | list [str ] | None ) -> list [str ]:
297+ """Normalize path value to list of strings."""
298+ if value is None :
299+ return []
300+ if isinstance (value , str ):
301+ return [value ]
302+ return value
303+
304+ def _discover_md_files_from_paths (
305+ self ,
306+ parser : SlashCommandParser | SubagentParser ,
307+ plugin_dir : Path ,
308+ paths : list [str ],
309+ plugin_info : PluginInfo | None ,
310+ seen_paths : set [Path ],
311+ ) -> list [Customization ]:
312+ """Discover markdown-based customizations from custom paths."""
313+ customizations : list [Customization ] = []
314+
315+ for path_str in paths :
316+ target = (plugin_dir / path_str ).resolve ()
317+
318+ if target .is_file () and target .suffix == ".md" :
319+ if target not in seen_paths :
320+ c = parser .parse (target , ConfigLevel .PLUGIN )
321+ if plugin_info :
322+ c .plugin_info = plugin_info
323+ customizations .append (c )
324+ seen_paths .add (target )
325+ elif target .is_dir ():
326+ for md_file in target .rglob ("*.md" ):
327+ resolved = md_file .resolve ()
328+ if resolved not in seen_paths :
329+ c = parser .parse (md_file , ConfigLevel .PLUGIN )
330+ if plugin_info :
331+ c .plugin_info = plugin_info
332+ customizations .append (c )
333+ seen_paths .add (resolved )
334+
335+ return customizations
336+
337+ def _discover_custom_skills (
338+ self ,
339+ plugin_dir : Path ,
340+ paths : list [str ],
341+ plugin_info : PluginInfo | None ,
342+ seen_paths : set [Path ],
343+ ) -> list [Customization ]:
344+ """Discover skills from custom paths."""
345+ customizations : list [Customization ] = []
346+ parser = SkillParser (plugin_dir )
347+
348+ for path_str in paths :
349+ target = (plugin_dir / path_str ).resolve ()
350+
351+ if target .is_dir ():
352+ skill_file = target / "SKILL.md"
353+ if skill_file .is_file ():
354+ resolved = skill_file .resolve ()
355+ if resolved not in seen_paths :
356+ c = parser .parse (skill_file , ConfigLevel .PLUGIN )
357+ if plugin_info :
358+ c .plugin_info = plugin_info
359+ customizations .append (c )
360+ seen_paths .add (resolved )
361+
362+ return customizations
363+
364+ def _discover_custom_mcps (
365+ self ,
366+ plugin_dir : Path ,
367+ mcp_path : str ,
368+ plugin_info : PluginInfo | None ,
369+ ) -> list [Customization ]:
370+ """Discover MCPs from custom path in marketplace.json."""
371+ customizations : list [Customization ] = []
372+ mcp_file = (plugin_dir / mcp_path ).resolve ()
373+
374+ if not mcp_file .is_file ():
375+ return customizations
376+
377+ parser = MCPParser ()
378+ for customization in parser .parse (mcp_file , ConfigLevel .PLUGIN ):
379+ if plugin_info :
380+ customization .plugin_info = plugin_info
381+ customizations .append (customization )
382+
383+ return customizations
384+
385+ def _discover_custom_hooks (
386+ self ,
387+ plugin_dir : Path ,
388+ hooks_path : str ,
389+ plugin_info : PluginInfo | None ,
390+ ) -> list [Customization ]:
391+ """Discover hooks from custom path in marketplace.json."""
392+ customizations : list [Customization ] = []
393+ hooks_file = (plugin_dir / hooks_path ).resolve ()
394+
395+ if not hooks_file .is_file ():
396+ return customizations
397+
398+ parser = HookParser ()
399+ for customization in parser .parse (hooks_file , ConfigLevel .PLUGIN ):
400+ if plugin_info :
401+ customization .plugin_info = plugin_info
402+ customizations .append (customization )
403+
404+ return customizations
405+
227406 def _discover_memory_files (self ) -> list [Customization ]:
228407 """Discover memory files from user and project levels."""
229408 customizations : list [Customization ] = []
0 commit comments