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,474 @@
|
|
|
1
|
+
"""Claude Code adapter for python-skills."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..adapters.base import (
|
|
7
|
+
AgentAdapter,
|
|
8
|
+
DetectionResult,
|
|
9
|
+
InstallResult,
|
|
10
|
+
StatusResult,
|
|
11
|
+
SyncResult,
|
|
12
|
+
UninstallResult,
|
|
13
|
+
)
|
|
14
|
+
from ..config import Target
|
|
15
|
+
from ..markers import BEGIN_MARKER, END_MARKER
|
|
16
|
+
from ..skills.registry import SkillRegistry
|
|
17
|
+
from ..state import LockManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClaudeAdapter(AgentAdapter):
|
|
21
|
+
"""Adapter for Claude Code."""
|
|
22
|
+
|
|
23
|
+
name = "Claude Code"
|
|
24
|
+
target = Target.CLAUDE.value
|
|
25
|
+
|
|
26
|
+
def __init__(self, project_root: Path, skills_registry: SkillRegistry, lock_manager: LockManager, config):
|
|
27
|
+
super().__init__(project_root, skills_registry, lock_manager, config)
|
|
28
|
+
|
|
29
|
+
def _get_global_path(self) -> Path:
|
|
30
|
+
"""Get the global Claude config path."""
|
|
31
|
+
return Path.home() / ".claude"
|
|
32
|
+
|
|
33
|
+
def detect(self) -> DetectionResult:
|
|
34
|
+
"""Detect Claude Code installation and configuration."""
|
|
35
|
+
# Check if claude is in PATH
|
|
36
|
+
app_detected = shutil.which("claude") is not None
|
|
37
|
+
|
|
38
|
+
# Check for project config
|
|
39
|
+
project_config = (self.project_root / ".claude").exists()
|
|
40
|
+
|
|
41
|
+
# Check for global config
|
|
42
|
+
global_config = self._get_global_path().exists()
|
|
43
|
+
|
|
44
|
+
return DetectionResult(
|
|
45
|
+
application_detected=app_detected,
|
|
46
|
+
project_config_detected=project_config,
|
|
47
|
+
adapter_available=True,
|
|
48
|
+
details=f"App: {'yes' if app_detected else 'no'}, Project config: {'yes' if project_config else 'no'}, Global config: {'yes' if global_config else 'no'}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
|
|
52
|
+
"""Install python-skills for Claude Code."""
|
|
53
|
+
result = InstallResult(success=True, target=self.target, scope=scope)
|
|
54
|
+
target_path = self._get_scope_path(scope)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# 1. Install native skills to .claude/skills/
|
|
58
|
+
skills_dir = target_path / ".claude" / "skills"
|
|
59
|
+
skills_created = self._install_skills(skills_dir, dry_run)
|
|
60
|
+
result.files_created.extend(skills_created)
|
|
61
|
+
|
|
62
|
+
# 2. Generate path-scoped rules
|
|
63
|
+
rules_created = self._install_rules(target_path, dry_run)
|
|
64
|
+
result.files_created.extend(rules_created)
|
|
65
|
+
|
|
66
|
+
# 3. Generate/update CLAUDE.md bootstrap
|
|
67
|
+
claude_md = self._generate_claude_md(target_path, dry_run)
|
|
68
|
+
if claude_md:
|
|
69
|
+
result.files_created.append(claude_md)
|
|
70
|
+
|
|
71
|
+
# 4. Update lock state
|
|
72
|
+
if not dry_run:
|
|
73
|
+
self._update_lock_state(scope)
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
result.success = False
|
|
77
|
+
result.errors.append(str(e))
|
|
78
|
+
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
def _install_skills(self, skills_dir: Path, dry_run: bool) -> list[str]:
|
|
82
|
+
"""Install canonical skills to .claude/skills/."""
|
|
83
|
+
created = []
|
|
84
|
+
skills_root = self.skills_registry.skills_root
|
|
85
|
+
|
|
86
|
+
if not skills_root.exists():
|
|
87
|
+
return created
|
|
88
|
+
|
|
89
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
|
|
91
|
+
# Copy each skill directory
|
|
92
|
+
for category_dir in skills_root.iterdir():
|
|
93
|
+
if not category_dir.is_dir():
|
|
94
|
+
continue
|
|
95
|
+
for skill_dir in category_dir.iterdir():
|
|
96
|
+
if not skill_dir.is_dir():
|
|
97
|
+
continue
|
|
98
|
+
skill_file = skill_dir / "SKILL.md"
|
|
99
|
+
if not skill_file.exists():
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
target_skill_dir = skills_dir / skill_dir.name
|
|
103
|
+
if dry_run:
|
|
104
|
+
created.append(str(target_skill_dir.relative_to(self.project_root)))
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Copy entire skill directory
|
|
108
|
+
if target_skill_dir.exists():
|
|
109
|
+
shutil.rmtree(target_skill_dir)
|
|
110
|
+
shutil.copytree(skill_dir, target_skill_dir)
|
|
111
|
+
|
|
112
|
+
created.append(str(target_skill_dir.relative_to(self.project_root)))
|
|
113
|
+
|
|
114
|
+
# Record in lock state
|
|
115
|
+
self._record_file(self.target, "project", target_skill_dir / "SKILL.md")
|
|
116
|
+
|
|
117
|
+
return created
|
|
118
|
+
|
|
119
|
+
def _install_rules(self, target_path: Path, dry_run: bool) -> list[str]:
|
|
120
|
+
"""Install path-scoped rules for Python engineering."""
|
|
121
|
+
created = []
|
|
122
|
+
rules_dir = target_path / ".claude" / "rules"
|
|
123
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
# Generate core Python rules
|
|
126
|
+
rules = [
|
|
127
|
+
("python-typing.md", self._generate_typing_rule()),
|
|
128
|
+
("python-testing.md", self._generate_testing_rule()),
|
|
129
|
+
("python-security.md", self._generate_security_rule()),
|
|
130
|
+
("python-async.md", self._generate_async_rule()),
|
|
131
|
+
("python-engineering.md", self._generate_engineering_rule()),
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
for filename, content in rules:
|
|
135
|
+
rule_path = rules_dir / filename
|
|
136
|
+
if dry_run:
|
|
137
|
+
created.append(str(rule_path.relative_to(self.project_root)))
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# Add ownership markers
|
|
141
|
+
marked_content = self._wrap_managed(content, rule_path)
|
|
142
|
+
self._safe_write_file(rule_path, marked_content, dry_run)
|
|
143
|
+
created.append(str(rule_path.relative_to(self.project_root)))
|
|
144
|
+
|
|
145
|
+
if not dry_run:
|
|
146
|
+
self._record_file(self.target, "project", rule_path)
|
|
147
|
+
|
|
148
|
+
return created
|
|
149
|
+
|
|
150
|
+
def _generate_claude_md(self, target_path: Path, dry_run: bool) -> str | None:
|
|
151
|
+
"""Generate/update CLAUDE.md bootstrap."""
|
|
152
|
+
claude_md = target_path / "CLAUDE.md"
|
|
153
|
+
|
|
154
|
+
if dry_run:
|
|
155
|
+
return str(claude_md.relative_to(self.project_root))
|
|
156
|
+
|
|
157
|
+
# Read existing or create new
|
|
158
|
+
existing = ""
|
|
159
|
+
if claude_md.exists():
|
|
160
|
+
existing = claude_md.read_text(encoding="utf-8")
|
|
161
|
+
|
|
162
|
+
# Generate bootstrap content
|
|
163
|
+
bootstrap = self._generate_claude_md_content()
|
|
164
|
+
|
|
165
|
+
if existing:
|
|
166
|
+
# Replace or append managed section
|
|
167
|
+
new_content, replaced = self._replace_managed_region(existing, bootstrap, claude_md)
|
|
168
|
+
if replaced:
|
|
169
|
+
self._safe_write_file(claude_md, new_content, dry_run)
|
|
170
|
+
self._record_file(self.target, "project", claude_md, (BEGIN_MARKER, END_MARKER))
|
|
171
|
+
return str(claude_md.relative_to(self.project_root))
|
|
172
|
+
# If no managed region, append
|
|
173
|
+
new_content = existing + "\n\n" + self._wrap_managed(bootstrap, claude_md)
|
|
174
|
+
self._safe_write_file(claude_md, new_content, dry_run)
|
|
175
|
+
self._record_file(self.target, "project", claude_md, (BEGIN_MARKER, END_MARKER))
|
|
176
|
+
else:
|
|
177
|
+
# Create new with bootstrap
|
|
178
|
+
full_content = f"# Project Instructions\n\n{bootstrap}"
|
|
179
|
+
self._safe_write_file(claude_md, self._wrap_managed(full_content, claude_md), dry_run)
|
|
180
|
+
self._record_file(self.target, "project", claude_md, (BEGIN_MARKER, END_MARKER))
|
|
181
|
+
|
|
182
|
+
return str(claude_md.relative_to(self.project_root))
|
|
183
|
+
|
|
184
|
+
def _generate_claude_md_content(self) -> str:
|
|
185
|
+
"""Generate CLAUDE.md bootstrap content."""
|
|
186
|
+
return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
|
|
187
|
+
# Python Skills Integration
|
|
188
|
+
|
|
189
|
+
This project uses [python-skills](https://github.com/FoxPink-dev/python-skills) for Python engineering standards.
|
|
190
|
+
|
|
191
|
+
## Available Skills
|
|
192
|
+
|
|
193
|
+
The following skill categories are available in `.claude/skills/`:
|
|
194
|
+
|
|
195
|
+
- **core/** - Core Python language patterns
|
|
196
|
+
- **stdlib/** - Standard library usage
|
|
197
|
+
- **generation/** - Code generation workflows
|
|
198
|
+
- **engineering/** - Project engineering practices
|
|
199
|
+
- **quality/** - Code quality standards
|
|
200
|
+
- **security/** - Security engineering
|
|
201
|
+
- **testing/** - Testing methodologies
|
|
202
|
+
- **refactoring/** - Safe refactoring patterns
|
|
203
|
+
- **debugging/** - Debugging techniques
|
|
204
|
+
- **anti_patterns/** - Anti-pattern prevention
|
|
205
|
+
|
|
206
|
+
## Usage
|
|
207
|
+
|
|
208
|
+
Skills are loaded automatically when relevant. You can also invoke them directly:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
/skill-name
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
See `.claude/skills/<skill-name>/SKILL.md` for each skill's description and usage.
|
|
215
|
+
<!-- END PYTHON-SKILLS MANAGED -->"""
|
|
216
|
+
|
|
217
|
+
def _generate_typing_rule(self) -> str:
|
|
218
|
+
return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
|
|
219
|
+
---
|
|
220
|
+
description: "Python typing engineering guidance"
|
|
221
|
+
globs: ["**/*.py"]
|
|
222
|
+
alwaysApply: false
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
# Python Typing Standards
|
|
226
|
+
|
|
227
|
+
## Type Hints
|
|
228
|
+
- Use built-in generics (Python 3.9+): `list[str]`, `dict[str, int]`
|
|
229
|
+
- Union types: `int | str` (3.10+) or `Union[int, str]`
|
|
230
|
+
- Optional: `str | None` not `Optional[str]`
|
|
231
|
+
|
|
232
|
+
## Protocols & Generics
|
|
233
|
+
- Use `Protocol` for structural typing
|
|
234
|
+
- `TypeVar` with constraints for generics
|
|
235
|
+
- `@dataclass` with type hints
|
|
236
|
+
|
|
237
|
+
## Validation
|
|
238
|
+
- Use Pydantic for runtime validation
|
|
239
|
+
- `TypedDict` for structured dicts
|
|
240
|
+
<!-- END PYTHON-SKILLS MANAGED -->"""
|
|
241
|
+
|
|
242
|
+
def _generate_testing_rule(self) -> str:
|
|
243
|
+
return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
|
|
244
|
+
---
|
|
245
|
+
description: "Python testing best practices"
|
|
246
|
+
globs: ["**/*test*.py", "**/test_*.py", "**/*_test.py"]
|
|
247
|
+
alwaysApply: false
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
# Testing Standards
|
|
251
|
+
|
|
252
|
+
## pytest
|
|
253
|
+
- Use `pytest` with `pytest-asyncio` for async
|
|
254
|
+
- Fixtures in `conftest.py`
|
|
255
|
+
- `pytest.mark.parametrize` for parameterized tests
|
|
256
|
+
|
|
257
|
+
## Coverage
|
|
258
|
+
- Target 90%+ branch coverage
|
|
259
|
+
- Exclude: `__repr__`, `raise AssertionError`, `if __name__ == "__main__"`
|
|
260
|
+
|
|
261
|
+
## Async Testing
|
|
262
|
+
- Use `pytest-asyncio` mode="auto"
|
|
263
|
+
- Test async functions with `await`
|
|
264
|
+
<!-- END PYTHON-SKILLS MANAGED -->"""
|
|
265
|
+
|
|
266
|
+
def _generate_security_rule(self) -> str:
|
|
267
|
+
return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
|
|
268
|
+
---
|
|
269
|
+
description: "Python security engineering"
|
|
270
|
+
globs: ["**/*.py"]
|
|
271
|
+
alwaysApply: false
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
# Security Standards
|
|
275
|
+
|
|
276
|
+
## Input Validation
|
|
277
|
+
- Validate all external input at boundaries
|
|
278
|
+
- Use allowlists, not blocklists
|
|
279
|
+
- Pydantic for API validation
|
|
280
|
+
|
|
281
|
+
## Injection Prevention
|
|
282
|
+
- SQL: parameterized queries only
|
|
283
|
+
- Command: list form, no shell=True
|
|
284
|
+
- Path: resolve + is_relative_to()
|
|
285
|
+
- Deserialization: json/yaml.safe_load only
|
|
286
|
+
<!-- END PYTHON-SKILLS MANAGED -->"""
|
|
287
|
+
|
|
288
|
+
def _generate_async_rule(self) -> str:
|
|
289
|
+
return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
|
|
290
|
+
---
|
|
291
|
+
description: "Python async/concurrency patterns"
|
|
292
|
+
globs: ["**/*.py"]
|
|
293
|
+
alwaysApply: false
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
# Async Patterns
|
|
297
|
+
|
|
298
|
+
## asyncio
|
|
299
|
+
- Use `asyncio.gather()` for concurrency
|
|
300
|
+
- `asyncio.Semaphore` for limiting
|
|
301
|
+
- `asyncio.TaskGroup` (3.11+) for structured concurrency
|
|
302
|
+
|
|
303
|
+
## Timeouts
|
|
304
|
+
- Always set timeouts: `asyncio.wait_for(coro, timeout=5.0)`
|
|
305
|
+
- Handle `asyncio.TimeoutError`
|
|
306
|
+
|
|
307
|
+
## Cancellation
|
|
308
|
+
- Check `task.cancelled()` in long operations
|
|
309
|
+
- Use `asyncio.shield()` for critical sections
|
|
310
|
+
<!-- END PYTHON-SKILLS MANAGED -->"""
|
|
311
|
+
|
|
312
|
+
def _generate_engineering_rule(self) -> str:
|
|
313
|
+
return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
|
|
314
|
+
---
|
|
315
|
+
description: "Python engineering practices"
|
|
316
|
+
globs: ["**/*.py"]
|
|
317
|
+
alwaysApply: false
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
# Engineering Practices
|
|
321
|
+
|
|
322
|
+
## Project Structure
|
|
323
|
+
- Use src-layout: `src/package/`
|
|
324
|
+
- `pyproject.toml` with hatchling
|
|
325
|
+
- `CLAUDE.md` for project instructions
|
|
326
|
+
|
|
327
|
+
## Configuration
|
|
328
|
+
- Layered: defaults → file → env → CLI
|
|
329
|
+
- Pydantic Settings for validation
|
|
330
|
+
- Environment variables for secrets
|
|
331
|
+
|
|
332
|
+
## HTTP Clients
|
|
333
|
+
- Use `httpx` (sync + async)
|
|
334
|
+
- Connection pooling with limits
|
|
335
|
+
- Retry with tenacity/httpx retries
|
|
336
|
+
|
|
337
|
+
## Packaging
|
|
338
|
+
- Use `build` (PEP 517)
|
|
339
|
+
- Version from `__version__` in package
|
|
340
|
+
- Publish with twine
|
|
341
|
+
<!-- END PYTHON-SKILLS MANAGED -->"""
|
|
342
|
+
|
|
343
|
+
def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
|
|
344
|
+
result = SyncResult(success=True, target=self.target, scope=scope)
|
|
345
|
+
target_path = self._get_scope_path(scope)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
# Reload canonical skills
|
|
349
|
+
skills_root = self.skills_registry.skills_root
|
|
350
|
+
|
|
351
|
+
# Update skills
|
|
352
|
+
skills_dir = target_path / ".claude" / "skills"
|
|
353
|
+
if skills_dir.exists():
|
|
354
|
+
for skill_dir in skills_dir.iterdir():
|
|
355
|
+
if skill_dir.is_dir():
|
|
356
|
+
# Check if skill still exists in canonical
|
|
357
|
+
canonical = skills_root / skill_dir.name
|
|
358
|
+
if canonical.exists():
|
|
359
|
+
# Update if changed
|
|
360
|
+
pass
|
|
361
|
+
else:
|
|
362
|
+
# Skill removed
|
|
363
|
+
if not dry_run:
|
|
364
|
+
shutil.rmtree(skill_dir)
|
|
365
|
+
result.removed.append(str(skill_dir.relative_to(self.project_root)))
|
|
366
|
+
|
|
367
|
+
# Add new skills
|
|
368
|
+
for category_dir in skills_root.iterdir():
|
|
369
|
+
if not category_dir.is_dir():
|
|
370
|
+
continue
|
|
371
|
+
for skill_dir in category_dir.iterdir():
|
|
372
|
+
if not skill_dir.is_dir():
|
|
373
|
+
continue
|
|
374
|
+
target_skill_dir = skills_dir / skill_dir.name
|
|
375
|
+
if not target_skill_dir.exists():
|
|
376
|
+
if not dry_run:
|
|
377
|
+
shutil.copytree(skill_dir, target_skill_dir)
|
|
378
|
+
result.added.append(str(target_skill_dir.relative_to(self.project_root)))
|
|
379
|
+
|
|
380
|
+
# Update lock state
|
|
381
|
+
if not dry_run:
|
|
382
|
+
self._update_lock_state(scope)
|
|
383
|
+
|
|
384
|
+
except Exception as e:
|
|
385
|
+
result.success = False
|
|
386
|
+
result.errors.append(str(e))
|
|
387
|
+
|
|
388
|
+
return result
|
|
389
|
+
|
|
390
|
+
def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
|
|
391
|
+
result = UninstallResult(success=True, target=self.target, scope=scope)
|
|
392
|
+
target_path = self._get_scope_path(scope)
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
# Remove .claude/skills/
|
|
396
|
+
skills_dir = target_path / ".claude" / "skills"
|
|
397
|
+
if skills_dir.exists():
|
|
398
|
+
if not dry_run:
|
|
399
|
+
shutil.rmtree(skills_dir)
|
|
400
|
+
result.files_removed.append(str(skills_dir.relative_to(self.project_root)))
|
|
401
|
+
|
|
402
|
+
# Remove rules
|
|
403
|
+
rules_dir = target_path / ".claude" / "rules"
|
|
404
|
+
if rules_dir.exists():
|
|
405
|
+
for rule_file in rules_dir.glob("python-*.md"):
|
|
406
|
+
if self._has_ownership_marker(rule_file):
|
|
407
|
+
if not dry_run:
|
|
408
|
+
rule_file.unlink()
|
|
409
|
+
result.files_removed.append(str(rule_file.relative_to(self.project_root)))
|
|
410
|
+
|
|
411
|
+
# Remove CLAUDE.md managed section
|
|
412
|
+
claude_md = target_path / "CLAUDE.md"
|
|
413
|
+
if claude_md.exists():
|
|
414
|
+
content = claude_md.read_text(encoding="utf-8")
|
|
415
|
+
new_content, removed = self._remove_managed_region(content, claude_md)
|
|
416
|
+
if removed and not dry_run:
|
|
417
|
+
self._safe_write_file(claude_md, new_content, dry_run)
|
|
418
|
+
result.regions_removed.append(str(claude_md.relative_to(self.project_root)))
|
|
419
|
+
|
|
420
|
+
if not dry_run:
|
|
421
|
+
self._update_lock_state(scope)
|
|
422
|
+
|
|
423
|
+
except Exception as e:
|
|
424
|
+
result.success = False
|
|
425
|
+
result.errors.append(str(e))
|
|
426
|
+
|
|
427
|
+
return result
|
|
428
|
+
|
|
429
|
+
def _has_ownership_marker(self, path: Path) -> bool:
|
|
430
|
+
"""Check if file has python-skills ownership marker."""
|
|
431
|
+
if not path.exists():
|
|
432
|
+
return False
|
|
433
|
+
content = path.read_text(encoding="utf-8")
|
|
434
|
+
begin, end = self._get_markers(path)
|
|
435
|
+
return begin in content and end in content
|
|
436
|
+
|
|
437
|
+
def status(self, scope: str = "project") -> StatusResult:
|
|
438
|
+
target_path = self._get_scope_path(scope)
|
|
439
|
+
installed = False
|
|
440
|
+
files = []
|
|
441
|
+
version = ""
|
|
442
|
+
|
|
443
|
+
# Check skills dir
|
|
444
|
+
skills_dir = target_path / ".claude" / "skills"
|
|
445
|
+
if skills_dir.exists():
|
|
446
|
+
installed = True
|
|
447
|
+
for f in skills_dir.rglob("SKILL.md"):
|
|
448
|
+
files.append(str(f.relative_to(self.project_root)))
|
|
449
|
+
|
|
450
|
+
# Check rules
|
|
451
|
+
rules_dir = target_path / ".claude" / "rules"
|
|
452
|
+
if rules_dir.exists():
|
|
453
|
+
for f in rules_dir.glob("python-*.md"):
|
|
454
|
+
if self._has_ownership_marker(f):
|
|
455
|
+
installed = True
|
|
456
|
+
files.append(str(f.relative_to(self.project_root)))
|
|
457
|
+
|
|
458
|
+
# Check CLAUDE.md
|
|
459
|
+
claude_md = target_path / "CLAUDE.md"
|
|
460
|
+
if claude_md.exists() and self._has_ownership_marker(claude_md):
|
|
461
|
+
installed = True
|
|
462
|
+
files.append(str(claude_md.relative_to(self.project_root)))
|
|
463
|
+
|
|
464
|
+
return StatusResult(
|
|
465
|
+
target=self.target,
|
|
466
|
+
scope=scope,
|
|
467
|
+
installed=installed,
|
|
468
|
+
files=files,
|
|
469
|
+
version=self.config.version if hasattr(self.config, 'version') else "1.0.0"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def _update_lock_state(self, scope: str) -> None:
|
|
473
|
+
"""Update lock state after changes."""
|
|
474
|
+
self.lock_manager.update_canonical_hash(self.skills_registry.skills_root)
|