-
Notifications
You must be signed in to change notification settings - Fork 146
Expand file tree
/
Copy pathplugin.py
More file actions
147 lines (126 loc) · 5.07 KB
/
plugin.py
File metadata and controls
147 lines (126 loc) · 5.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""Plugin management data models."""
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Dict, Any
import json
@dataclass
class PluginMetadata:
"""Metadata for a plugin.
Attributes:
id: Unique plugin identifier (e.g., "awesome-copilot")
name: Human-readable plugin name
version: Semantic version string
description: Short description of the plugin
author: Plugin author name or organization
repository: Repository reference (e.g., "owner/repo" or "dev.azure.com/org/project/repo")
homepage: Optional homepage URL
license: Optional license identifier (e.g., "MIT", "Apache-2.0")
tags: List of tags for categorization
dependencies: List of plugin dependencies (plugin IDs)
"""
id: str
name: str
version: str
description: str
author: str
repository: Optional[str] = None
homepage: Optional[str] = None
license: Optional[str] = None
tags: List[str] = field(default_factory=list)
dependencies: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
"""Convert metadata to dictionary."""
return {
"id": self.id,
"name": self.name,
"version": self.version,
"description": self.description,
"author": self.author,
"repository": self.repository,
"homepage": self.homepage,
"license": self.license,
"tags": self.tags,
"dependencies": self.dependencies,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "PluginMetadata":
"""Create metadata from dictionary."""
return cls(
id=data["id"],
name=data["name"],
version=data["version"],
description=data["description"],
author=data["author"],
repository=data.get("repository"),
homepage=data.get("homepage"),
license=data.get("license"),
tags=data.get("tags", []),
dependencies=data.get("dependencies", []),
)
@dataclass
class Plugin:
"""Represents an installed plugin.
Attributes:
metadata: Plugin metadata
path: Path to the plugin directory
commands: List of command file paths
agents: List of agent file paths (*.agent.md)
hooks: List of hook script paths
skills: List of skill file paths (*.skill.md)
"""
metadata: PluginMetadata
path: Path
commands: List[Path] = field(default_factory=list)
agents: List[Path] = field(default_factory=list)
hooks: List[Path] = field(default_factory=list)
skills: List[Path] = field(default_factory=list)
@classmethod
def from_path(cls, plugin_path: Path) -> "Plugin":
"""Load a plugin from its installation directory.
Plugin structure: plugin.json can be in root, .github/plugin/, or .claude-plugin/.
Primitives (agents, skills, etc.) are always at the repository root.
Args:
plugin_path: Path to the plugin directory
Returns:
Plugin: The loaded plugin instance
Raises:
FileNotFoundError: If plugin.json is not found
ValueError: If plugin.json is invalid
"""
# Find plugin.json using centralized helper
from ..utils.helpers import find_plugin_json
metadata_file = find_plugin_json(plugin_path)
if metadata_file is None:
raise FileNotFoundError(f"Plugin metadata not found in any expected location: {plugin_path}")
with open(metadata_file, "r", encoding="utf-8") as f:
metadata_dict = json.load(f)
metadata = PluginMetadata.from_dict(metadata_dict)
# Primitives are always at the repository root
base_dir = plugin_path
# Discover plugin components in plugins/ subdirectory (including subdirectories)
commands = list((base_dir / "commands").rglob("*.py")) if (base_dir / "commands").exists() else []
# Agents: include both .agent.md and plain .md (plugins may omit the
# .agent.md convention).
agents = []
if (base_dir / "agents").exists():
agents = [
f for f in (base_dir / "agents").rglob("*.md")
]
hooks = list((base_dir / "hooks").rglob("*.py")) if (base_dir / "hooks").exists() else []
# Skills: each subdirectory in skills/ must contain a SKILL.md
skills = []
skills_dir = base_dir / "skills"
if skills_dir.exists():
for skill_subdir in skills_dir.iterdir():
if skill_subdir.is_dir():
skill_file = skill_subdir / "SKILL.md"
if skill_file.exists():
skills.append(skill_file)
return cls(
metadata=metadata,
path=plugin_path,
commands=commands,
agents=agents,
hooks=hooks,
skills=skills,
)