python-skills 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_skills/__init__.py +10 -0
- python_skills/__main__.py +6 -0
- python_skills/adapters/__init__.py +48 -0
- python_skills/adapters/agent_skills.py +415 -0
- python_skills/adapters/aider_adapter.py +226 -0
- python_skills/adapters/base.py +153 -0
- python_skills/adapters/claude.py +474 -0
- python_skills/adapters/cline.py +332 -0
- python_skills/adapters/codex.py +24 -0
- python_skills/adapters/continue_adapter.py +198 -0
- python_skills/adapters/cursor.py +327 -0
- python_skills/adapters/gemini.py +26 -0
- python_skills/adapters/goose.py +26 -0
- python_skills/adapters/junie.py +25 -0
- python_skills/adapters/kiro.py +382 -0
- python_skills/adapters/opencode.py +27 -0
- python_skills/adapters/roo.py +25 -0
- python_skills/adapters/universal.py +203 -0
- python_skills/adapters/vscode.py +27 -0
- python_skills/adapters/windsurf.py +26 -0
- python_skills/adapters/zed.py +27 -0
- python_skills/cli.py +326 -0
- python_skills/config.py +160 -0
- python_skills/detector.py +152 -0
- python_skills/installer.py +163 -0
- python_skills/markers.py +115 -0
- python_skills/skills/__init__.py +14 -0
- python_skills/skills/loader.py +171 -0
- python_skills/skills/metadata.py +152 -0
- python_skills/skills/registry.py +101 -0
- python_skills/state.py +204 -0
- python_skills-1.0.0.dist-info/METADATA +99 -0
- python_skills-1.0.0.dist-info/RECORD +105 -0
- python_skills-1.0.0.dist-info/WHEEL +4 -0
- python_skills-1.0.0.dist-info/entry_points.txt +2 -0
- python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
- skills/advanced_python.md +239 -0
- skills/anti_patterns/index.md +406 -0
- skills/comprehensions.md +167 -0
- skills/control_flow.md +175 -0
- skills/data_structures.md +243 -0
- skills/debugging/common_bugs.md +222 -0
- skills/debugging/inspection_techniques.md +249 -0
- skills/debugging/root_cause.md +203 -0
- skills/engineering/application_logging.md +195 -0
- skills/engineering/cli_apps.md +207 -0
- skills/engineering/configuration.md +218 -0
- skills/engineering/database.md +240 -0
- skills/engineering/dependency_management.md +205 -0
- skills/engineering/http_clients.md +267 -0
- skills/engineering/modules_packages.md +211 -0
- skills/engineering/packaging.md +197 -0
- skills/engineering/project_structure.md +155 -0
- skills/engineering/pyproject_toml.md +302 -0
- skills/engineering/virtual_environments.md +206 -0
- skills/functions.md +244 -0
- skills/generation/async_concurrency.md +291 -0
- skills/generation/error_handling.md +276 -0
- skills/generation/protocols_generics.md +243 -0
- skills/generation/type_hints.md +290 -0
- skills/generation/validation_pipeline.md +274 -0
- skills/generation/workflow.md +190 -0
- skills/oop.md +228 -0
- skills/quality/abstractions.md +154 -0
- skills/quality/comments.md +177 -0
- skills/quality/documentation.md +176 -0
- skills/quality/duplication.md +137 -0
- skills/quality/maintainability.md +142 -0
- skills/quality/naming.md +171 -0
- skills/quality/quality_functions.md +245 -0
- skills/quality/readability.md +239 -0
- skills/quality/type_annotations.md +192 -0
- skills/refactoring/behavior_preservation.md +157 -0
- skills/refactoring/incremental.md +187 -0
- skills/refactoring/interface_stability.md +199 -0
- skills/refactoring/safe_refactoring.md +206 -0
- skills/security/auth_boundaries.md +200 -0
- skills/security/command_injection.md +207 -0
- skills/security/dependency_risks.md +282 -0
- skills/security/file_handling.md +156 -0
- skills/security/input_validation.md +190 -0
- skills/security/path_traversal.md +172 -0
- skills/security/secrets.md +171 -0
- skills/security/sql_injection.md +188 -0
- skills/security/unsafe_deserialization.md +164 -0
- skills/stdlib/argparse.md +178 -0
- skills/stdlib/collections.md +212 -0
- skills/stdlib/datetime.md +187 -0
- skills/stdlib/functools.md +238 -0
- skills/stdlib/itertools.md +183 -0
- skills/stdlib/json.md +162 -0
- skills/stdlib/logging.md +185 -0
- skills/stdlib/os_sys.md +184 -0
- skills/stdlib/pathlib.md +218 -0
- skills/stdlib/re.md +171 -0
- skills/stdlib/statistics.md +112 -0
- skills/stdlib/subprocess.md +211 -0
- skills/testing/async_tests.md +249 -0
- skills/testing/coverage.md +168 -0
- skills/testing/edge_cases.md +197 -0
- skills/testing/fixtures_mocks.md +203 -0
- skills/testing/organization.md +205 -0
- skills/testing/parameterized.md +174 -0
- skills/testing/regression_tests.md +165 -0
- skills/variables_types.md +107 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Python Skills - Python engineering skills for AI coding agents."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version as _metadata_version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = _metadata_version("python-skills")
|
|
7
|
+
except Exception:
|
|
8
|
+
__version__ = "0.0.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Adapters for python-skills target environments."""
|
|
2
|
+
|
|
3
|
+
from ..config import Target
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_adapter(target: Target, project_root, skills_registry, lock_manager, config):
|
|
7
|
+
"""Factory function to get adapter instance by target."""
|
|
8
|
+
from .aider_adapter import AiderAdapter
|
|
9
|
+
from .claude import ClaudeAdapter
|
|
10
|
+
from .cline import ClineAdapter
|
|
11
|
+
from .codex import CodexAdapter
|
|
12
|
+
from .continue_adapter import ContinueAdapter
|
|
13
|
+
from .cursor import CursorAdapter
|
|
14
|
+
from .gemini import GeminiAdapter
|
|
15
|
+
from .goose import GooseAdapter
|
|
16
|
+
from .junie import JunieAdapter
|
|
17
|
+
from .kiro import KiroAdapter
|
|
18
|
+
from .opencode import OpenCodeAdapter
|
|
19
|
+
from .roo import RooAdapter
|
|
20
|
+
from .universal import UniversalAdapter
|
|
21
|
+
from .vscode import VSCodeAdapter
|
|
22
|
+
from .windsurf import WindsurfAdapter
|
|
23
|
+
from .zed import ZedAdapter
|
|
24
|
+
|
|
25
|
+
adapters = {
|
|
26
|
+
Target.CLAUDE: ClaudeAdapter,
|
|
27
|
+
Target.CURSOR: CursorAdapter,
|
|
28
|
+
Target.KIRO: KiroAdapter,
|
|
29
|
+
Target.CLINE: ClineAdapter,
|
|
30
|
+
Target.UNIVERSAL: UniversalAdapter,
|
|
31
|
+
Target.OPENCODE: OpenCodeAdapter,
|
|
32
|
+
Target.WINDSURF: WindsurfAdapter,
|
|
33
|
+
Target.VSCODE: VSCodeAdapter,
|
|
34
|
+
Target.ROO: RooAdapter,
|
|
35
|
+
Target.GEMINI: GeminiAdapter,
|
|
36
|
+
Target.CODEX: CodexAdapter,
|
|
37
|
+
Target.JETBRAINS: JunieAdapter,
|
|
38
|
+
Target.GOOSE: GooseAdapter,
|
|
39
|
+
Target.ZED: ZedAdapter,
|
|
40
|
+
Target.CONTINUE: ContinueAdapter,
|
|
41
|
+
Target.AIDER: AiderAdapter,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
adapter_cls = adapters.get(target)
|
|
45
|
+
if adapter_cls is None:
|
|
46
|
+
raise ValueError(f"Unknown target: {target}")
|
|
47
|
+
|
|
48
|
+
return adapter_cls(project_root, skills_registry, lock_manager, config)
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Shared Agent Skills adapter for targets supporting the Agent Skills specification.
|
|
2
|
+
|
|
3
|
+
The Agent Skills specification (https://agentskills.io) defines:
|
|
4
|
+
- SKILL.md with YAML frontmatter (name, description required)
|
|
5
|
+
- Directory-based skill structure with optional supporting files
|
|
6
|
+
- Progressive disclosure (metadata first, full content on demand)
|
|
7
|
+
|
|
8
|
+
This base class handles:
|
|
9
|
+
- Converting canonical flat .md skills to Agent Skills SKILL.md format
|
|
10
|
+
- Installing skills to target-specific discovery paths
|
|
11
|
+
- Sync, uninstall, and status operations
|
|
12
|
+
- Ownership tracking via markers
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import shutil
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from ..adapters.base import (
|
|
19
|
+
AgentAdapter,
|
|
20
|
+
DetectionResult,
|
|
21
|
+
InstallResult,
|
|
22
|
+
StatusResult,
|
|
23
|
+
SyncResult,
|
|
24
|
+
UninstallResult,
|
|
25
|
+
)
|
|
26
|
+
from ..skills.metadata import SkillMetadata
|
|
27
|
+
from ..skills.registry import SkillRegistry
|
|
28
|
+
from ..state import LockManager
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _skill_name_to_slug(name: str) -> str:
|
|
32
|
+
"""Convert a skill name to an Agent Skills compliant slug.
|
|
33
|
+
|
|
34
|
+
Agent Skills spec: lowercase alphanumeric with single hyphen separators.
|
|
35
|
+
"""
|
|
36
|
+
slug = name.lower()
|
|
37
|
+
slug = slug.replace("_", "-")
|
|
38
|
+
# Remove consecutive hyphens
|
|
39
|
+
while "--" in slug:
|
|
40
|
+
slug = slug.replace("--", "-")
|
|
41
|
+
# Strip leading/trailing hyphens
|
|
42
|
+
slug = slug.strip("-")
|
|
43
|
+
# Ensure it's valid
|
|
44
|
+
if not slug:
|
|
45
|
+
slug = "skill"
|
|
46
|
+
return slug
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _generate_skill_md(skill: SkillMetadata, content: str) -> str:
|
|
50
|
+
"""Generate Agent Skills compliant SKILL.md from a canonical skill.
|
|
51
|
+
|
|
52
|
+
Adds YAML frontmatter if missing, preserves existing content.
|
|
53
|
+
"""
|
|
54
|
+
# Check if content already has frontmatter
|
|
55
|
+
if content.startswith("---"):
|
|
56
|
+
return content
|
|
57
|
+
|
|
58
|
+
# Generate frontmatter
|
|
59
|
+
slug = _skill_name_to_slug(skill.name)
|
|
60
|
+
description = skill.description or f"Python {skill.category} skill: {skill.name}"
|
|
61
|
+
|
|
62
|
+
# Truncate description to 1024 chars (Agent Skills spec limit)
|
|
63
|
+
if len(description) > 1024:
|
|
64
|
+
description = description[:1021] + "..."
|
|
65
|
+
|
|
66
|
+
frontmatter = f"""---
|
|
67
|
+
name: {slug}
|
|
68
|
+
description: {description}
|
|
69
|
+
---"""
|
|
70
|
+
|
|
71
|
+
return f"{frontmatter}\n\n{content}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AgentSkillsAdapter(AgentAdapter):
|
|
75
|
+
"""Base adapter for targets supporting the Agent Skills specification.
|
|
76
|
+
|
|
77
|
+
Subclasses define:
|
|
78
|
+
- target_name: Target enum value
|
|
79
|
+
- display_name: Human-readable name
|
|
80
|
+
- agent_skills_dir: Project-level path for SKILL.md files (e.g., ".agents/skills")
|
|
81
|
+
- global_skills_dir: Global path for SKILL.md files
|
|
82
|
+
- detection_project_markers: Files/dirs that indicate project integration
|
|
83
|
+
- detection_app_command: CLI command to check for app detection
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
# Subclasses must set these
|
|
87
|
+
target_name: str = ""
|
|
88
|
+
display_name: str = ""
|
|
89
|
+
agent_skills_dir: str = ".agents/skills"
|
|
90
|
+
global_skills_dir: str = ""
|
|
91
|
+
detection_project_markers: list[str] = []
|
|
92
|
+
detection_app_command: str | None = None
|
|
93
|
+
# Additional target-native skill directory (e.g., ".opencode/skills")
|
|
94
|
+
native_skills_dir: str | None = None
|
|
95
|
+
|
|
96
|
+
def __init__(self, project_root: Path, skills_registry: SkillRegistry,
|
|
97
|
+
lock_manager: LockManager, config):
|
|
98
|
+
super().__init__(project_root, skills_registry, lock_manager, config)
|
|
99
|
+
|
|
100
|
+
def _get_global_path(self) -> Path:
|
|
101
|
+
if self.global_skills_dir:
|
|
102
|
+
return Path.home() / self.global_skills_dir
|
|
103
|
+
return Path.home() / ".agents"
|
|
104
|
+
|
|
105
|
+
def detect(self) -> DetectionResult:
|
|
106
|
+
"""Detect if this agent is available and configured."""
|
|
107
|
+
import shutil as _shutil
|
|
108
|
+
|
|
109
|
+
app_detected = False
|
|
110
|
+
if self.detection_app_command:
|
|
111
|
+
app_detected = _shutil.which(self.detection_app_command) is not None
|
|
112
|
+
|
|
113
|
+
project_config = any(
|
|
114
|
+
(self.project_root / marker).exists()
|
|
115
|
+
for marker in self.detection_project_markers
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
global_config = self._get_global_path().exists()
|
|
119
|
+
|
|
120
|
+
return DetectionResult(
|
|
121
|
+
application_detected=app_detected,
|
|
122
|
+
project_config_detected=project_config,
|
|
123
|
+
adapter_available=True,
|
|
124
|
+
details=f"App: {'yes' if app_detected else 'no'}, "
|
|
125
|
+
f"Project: {'yes' if project_config else 'no'}, "
|
|
126
|
+
f"Global: {'yes' if global_config else 'no'}",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
|
|
130
|
+
"""Install Agent Skills for this target."""
|
|
131
|
+
result = InstallResult(success=True, target=self.target_name, scope=scope)
|
|
132
|
+
target_path = self._get_scope_path(scope)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# 1. Install skills to .agents/skills/
|
|
136
|
+
skills_dir = target_path / self.agent_skills_dir
|
|
137
|
+
created = self._install_skills_to_dir(skills_dir, dry_run)
|
|
138
|
+
result.files_created.extend(created)
|
|
139
|
+
|
|
140
|
+
# 2. If target has a native skills dir that differs, also install there
|
|
141
|
+
if self.native_skills_dir and self.native_skills_dir != self.agent_skills_dir:
|
|
142
|
+
native_dir = target_path / self.native_skills_dir
|
|
143
|
+
native_created = self._install_skills_to_dir(native_dir, dry_run)
|
|
144
|
+
result.files_created.extend(native_created)
|
|
145
|
+
|
|
146
|
+
# 3. Generate instructions file if the target supports it
|
|
147
|
+
instructions_created = self._install_instructions(target_path, dry_run)
|
|
148
|
+
result.files_created.extend(instructions_created)
|
|
149
|
+
|
|
150
|
+
if not dry_run:
|
|
151
|
+
self._update_lock_state(scope)
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
result.success = False
|
|
155
|
+
result.errors.append(str(e))
|
|
156
|
+
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
def _install_skills_to_dir(self, skills_dir: Path, dry_run: bool) -> list[str]:
|
|
160
|
+
"""Install all canonical skills to a target skills directory."""
|
|
161
|
+
created = []
|
|
162
|
+
|
|
163
|
+
if not self.skills_registry:
|
|
164
|
+
return created
|
|
165
|
+
|
|
166
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
|
|
168
|
+
for skill in self.skills_registry.get_all_skills():
|
|
169
|
+
slug = _skill_name_to_slug(skill.name)
|
|
170
|
+
target_skill_dir = skills_dir / slug
|
|
171
|
+
target_skill_file = target_skill_dir / "SKILL.md"
|
|
172
|
+
|
|
173
|
+
if dry_run:
|
|
174
|
+
created.append(str(target_skill_file.relative_to(self.project_root)))
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# Get canonical content
|
|
178
|
+
content = self.skills_registry.get_skill_content(skill.name)
|
|
179
|
+
if not content:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
# Generate Agent Skills compliant SKILL.md
|
|
183
|
+
skill_md = _generate_skill_md(skill, content)
|
|
184
|
+
|
|
185
|
+
# Wrap with ownership markers for status/uninstall detection
|
|
186
|
+
skill_md = self._wrap_managed(skill_md, target_skill_file)
|
|
187
|
+
|
|
188
|
+
# Write to target
|
|
189
|
+
target_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
target_skill_file.write_text(skill_md, encoding="utf-8")
|
|
191
|
+
|
|
192
|
+
created.append(str(target_skill_file.relative_to(self.project_root)))
|
|
193
|
+
self._record_file(self.target_name, "project", target_skill_file)
|
|
194
|
+
|
|
195
|
+
return created
|
|
196
|
+
|
|
197
|
+
def _install_instructions(self, target_path: Path, dry_run: bool) -> list[str]:
|
|
198
|
+
"""Generate instructions file for the target. Override in subclasses."""
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
|
|
202
|
+
"""Sync installed skills with canonical source."""
|
|
203
|
+
result = SyncResult(success=True, target=self.target_name, scope=scope)
|
|
204
|
+
target_path = self._get_scope_path(scope)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
skills_dir = target_path / self.agent_skills_dir
|
|
208
|
+
|
|
209
|
+
if not skills_dir.exists():
|
|
210
|
+
result.success = False
|
|
211
|
+
result.errors.append(f"No {self.agent_skills_dir} directory found")
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
# Get current installed skills
|
|
215
|
+
installed = set()
|
|
216
|
+
for item in skills_dir.iterdir():
|
|
217
|
+
if item.is_dir() and (item / "SKILL.md").exists():
|
|
218
|
+
installed.add(item.name)
|
|
219
|
+
|
|
220
|
+
# Get canonical skills
|
|
221
|
+
canonical = set()
|
|
222
|
+
if self.skills_registry:
|
|
223
|
+
for skill in self.skills_registry.get_all_skills():
|
|
224
|
+
slug = _skill_name_to_slug(skill.name)
|
|
225
|
+
canonical.add(slug)
|
|
226
|
+
|
|
227
|
+
# Remove skills no longer in canonical
|
|
228
|
+
for skill_name in installed:
|
|
229
|
+
if skill_name not in canonical:
|
|
230
|
+
skill_dir = skills_dir / skill_name
|
|
231
|
+
if not dry_run:
|
|
232
|
+
shutil.rmtree(skill_dir)
|
|
233
|
+
result.removed.append(str(skill_dir.relative_to(self.project_root)))
|
|
234
|
+
|
|
235
|
+
# Add new skills
|
|
236
|
+
for slug in canonical:
|
|
237
|
+
if slug not in installed:
|
|
238
|
+
skill = self.skills_registry.get_skill(slug.replace("-", "_"))
|
|
239
|
+
if skill:
|
|
240
|
+
target_dir = skills_dir / slug
|
|
241
|
+
if not dry_run:
|
|
242
|
+
content = self.skills_registry.get_skill_content(skill.name)
|
|
243
|
+
if content:
|
|
244
|
+
skill_md = _generate_skill_md(skill, content)
|
|
245
|
+
skill_md = self._wrap_managed(skill_md, target_dir / "SKILL.md")
|
|
246
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
(target_dir / "SKILL.md").write_text(skill_md, encoding="utf-8")
|
|
248
|
+
result.added.append(str((target_dir / "SKILL.md").relative_to(self.project_root)))
|
|
249
|
+
|
|
250
|
+
# Update existing skills
|
|
251
|
+
for slug in canonical & installed:
|
|
252
|
+
skill = self.skills_registry.get_skill(slug.replace("-", "_"))
|
|
253
|
+
if skill:
|
|
254
|
+
skill_file = skills_dir / slug / "SKILL.md"
|
|
255
|
+
if skill_file.exists():
|
|
256
|
+
current = skill_file.read_text(encoding="utf-8")
|
|
257
|
+
content = self.skills_registry.get_skill_content(skill.name)
|
|
258
|
+
if content:
|
|
259
|
+
new_content = _generate_skill_md(skill, content)
|
|
260
|
+
# Wrap with markers for comparison and writing
|
|
261
|
+
new_content_wrapped = self._wrap_managed(new_content, skill_file)
|
|
262
|
+
# Strip markers from current to compare raw content
|
|
263
|
+
begin, end = self._get_markers(skill_file)
|
|
264
|
+
current_raw = current
|
|
265
|
+
if begin in current and end in current:
|
|
266
|
+
start = current.find(begin) + len(begin)
|
|
267
|
+
end_idx = current.find(end, start)
|
|
268
|
+
if end_idx != -1:
|
|
269
|
+
current_raw = current[start:end_idx].strip()
|
|
270
|
+
new_raw = new_content
|
|
271
|
+
if current_raw.strip() != new_raw.strip():
|
|
272
|
+
if not dry_run:
|
|
273
|
+
skill_file.write_text(new_content_wrapped, encoding="utf-8")
|
|
274
|
+
result.modified.append(str(skill_file.relative_to(self.project_root)))
|
|
275
|
+
else:
|
|
276
|
+
result.skipped.append(str(skill_file.relative_to(self.project_root)))
|
|
277
|
+
|
|
278
|
+
if not dry_run:
|
|
279
|
+
self._update_lock_state(scope)
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
result.success = False
|
|
283
|
+
result.errors.append(str(e))
|
|
284
|
+
|
|
285
|
+
return result
|
|
286
|
+
|
|
287
|
+
def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
|
|
288
|
+
"""Uninstall skills managed by python-skills.
|
|
289
|
+
|
|
290
|
+
For shared directories (.agents/skills/), only removes the vendor-specific
|
|
291
|
+
native directory. The shared directory is preserved for other consumers.
|
|
292
|
+
"""
|
|
293
|
+
result = UninstallResult(success=True, target=self.target_name, scope=scope)
|
|
294
|
+
target_path = self._get_scope_path(scope)
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
# Remove vendor-specific native skills dir (e.g., .opencode/skills/)
|
|
298
|
+
if self.native_skills_dir and self.native_skills_dir != self.agent_skills_dir:
|
|
299
|
+
native_dir = target_path / self.native_skills_dir
|
|
300
|
+
if native_dir.exists():
|
|
301
|
+
for item in native_dir.iterdir():
|
|
302
|
+
if item.is_dir():
|
|
303
|
+
skill_file = item / "SKILL.md"
|
|
304
|
+
if skill_file.exists():
|
|
305
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
306
|
+
begin, _end = self._get_markers(skill_file)
|
|
307
|
+
if begin in content:
|
|
308
|
+
if not dry_run:
|
|
309
|
+
shutil.rmtree(item)
|
|
310
|
+
result.files_removed.append(
|
|
311
|
+
str(item.relative_to(self.project_root))
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# For the shared .agents/skills/ dir: only remove if this target
|
|
315
|
+
# is the sole consumer. Check lock file for other consumers.
|
|
316
|
+
skills_dir = target_path / self.agent_skills_dir
|
|
317
|
+
if skills_dir.exists() and self.agent_skills_dir == ".agents/skills":
|
|
318
|
+
# Check if any other target still references .agents/skills/
|
|
319
|
+
other_consumers = self._count_other_consumers(scope)
|
|
320
|
+
if other_consumers > 0:
|
|
321
|
+
# Other consumers exist — don't touch shared dir
|
|
322
|
+
pass
|
|
323
|
+
else:
|
|
324
|
+
# No other consumers — safe to remove shared dir
|
|
325
|
+
for item in skills_dir.iterdir():
|
|
326
|
+
if item.is_dir():
|
|
327
|
+
skill_file = item / "SKILL.md"
|
|
328
|
+
if skill_file.exists():
|
|
329
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
330
|
+
begin, _end = self._get_markers(skill_file)
|
|
331
|
+
if begin in content:
|
|
332
|
+
if not dry_run:
|
|
333
|
+
shutil.rmtree(item)
|
|
334
|
+
result.files_removed.append(
|
|
335
|
+
str(item.relative_to(self.project_root))
|
|
336
|
+
)
|
|
337
|
+
# Remove the shared directory if empty
|
|
338
|
+
if not dry_run and skills_dir.exists():
|
|
339
|
+
remaining = list(skills_dir.iterdir())
|
|
340
|
+
if not remaining:
|
|
341
|
+
skills_dir.rmdir()
|
|
342
|
+
elif skills_dir.exists():
|
|
343
|
+
# Non-shared agent_skills_dir — remove normally
|
|
344
|
+
for item in skills_dir.iterdir():
|
|
345
|
+
if item.is_dir():
|
|
346
|
+
skill_file = item / "SKILL.md"
|
|
347
|
+
if skill_file.exists():
|
|
348
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
349
|
+
begin, _end = self._get_markers(skill_file)
|
|
350
|
+
if begin in content:
|
|
351
|
+
if not dry_run:
|
|
352
|
+
shutil.rmtree(item)
|
|
353
|
+
result.files_removed.append(
|
|
354
|
+
str(item.relative_to(self.project_root))
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if not dry_run:
|
|
358
|
+
self._update_lock_state(scope)
|
|
359
|
+
# Clear target state after uninstall
|
|
360
|
+
self.lock_manager.clear_target(self.target_name)
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
result.success = False
|
|
364
|
+
result.errors.append(str(e))
|
|
365
|
+
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
def _count_other_consumers(self, scope: str) -> int:
|
|
369
|
+
"""Count how many other targets use the shared .agents/skills/ directory."""
|
|
370
|
+
# Known targets that consume .agents/skills/
|
|
371
|
+
shared_consumers = {
|
|
372
|
+
"opencode", "windsurf", "vscode", "gemini",
|
|
373
|
+
"roo", "codex", "goose", "jetbrains", "zed",
|
|
374
|
+
}
|
|
375
|
+
# Remove self from the count
|
|
376
|
+
other = shared_consumers - {self.target_name}
|
|
377
|
+
# Check which others are actually installed (have lock records)
|
|
378
|
+
count = 0
|
|
379
|
+
for consumer in other:
|
|
380
|
+
status = self.lock_manager.get_target_status(consumer)
|
|
381
|
+
if status and status.files:
|
|
382
|
+
count += 1
|
|
383
|
+
return count
|
|
384
|
+
|
|
385
|
+
def status(self, scope: str = "project") -> StatusResult:
|
|
386
|
+
"""Get installation status."""
|
|
387
|
+
target_path = self._get_scope_path(scope)
|
|
388
|
+
installed = False
|
|
389
|
+
files = []
|
|
390
|
+
|
|
391
|
+
skills_dir = target_path / self.agent_skills_dir
|
|
392
|
+
if skills_dir.exists():
|
|
393
|
+
for item in skills_dir.iterdir():
|
|
394
|
+
if item.is_dir():
|
|
395
|
+
skill_file = item / "SKILL.md"
|
|
396
|
+
if skill_file.exists():
|
|
397
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
398
|
+
begin, end = self._get_markers(skill_file)
|
|
399
|
+
if begin in content:
|
|
400
|
+
installed = True
|
|
401
|
+
files.append(str(skill_file.relative_to(self.project_root)))
|
|
402
|
+
|
|
403
|
+
return StatusResult(
|
|
404
|
+
target=self.target_name,
|
|
405
|
+
scope=scope,
|
|
406
|
+
installed=installed,
|
|
407
|
+
files=files,
|
|
408
|
+
version="2.0.0",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def _update_lock_state(self, scope: str) -> None:
|
|
412
|
+
"""Update lock state after changes."""
|
|
413
|
+
skills_root = self.skills_registry.skills_root
|
|
414
|
+
if skills_root.exists():
|
|
415
|
+
self.lock_manager.update_canonical_hash(skills_root)
|