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,382 @@
|
|
|
1
|
+
"""Kiro 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 KiroAdapter(AgentAdapter):
|
|
21
|
+
"""Adapter for Kiro."""
|
|
22
|
+
|
|
23
|
+
name = "Kiro"
|
|
24
|
+
target = Target.KIRO.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
|
+
return Path.home() / ".kiro"
|
|
31
|
+
|
|
32
|
+
def detect(self) -> DetectionResult:
|
|
33
|
+
app_detected = shutil.which("kiro") is not None
|
|
34
|
+
project_config = (self.project_root / ".kiro").exists()
|
|
35
|
+
global_config = self._get_global_path().exists()
|
|
36
|
+
|
|
37
|
+
return DetectionResult(
|
|
38
|
+
application_detected=app_detected,
|
|
39
|
+
project_config_detected=project_config,
|
|
40
|
+
adapter_available=True,
|
|
41
|
+
details=f"App: {'yes' if app_detected else 'no'}, Project: {'yes' if project_config else 'no'}, Global: {'yes' if global_config else 'no'}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
|
|
45
|
+
result = InstallResult(success=True, target=self.target, scope=scope)
|
|
46
|
+
target_path = self._get_scope_path(scope)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
# 1. Install native skills to .kiro/skills/
|
|
50
|
+
skills_dir = target_path / ".kiro" / "skills"
|
|
51
|
+
skills_created = self._install_skills(skills_dir, dry_run)
|
|
52
|
+
result.files_created.extend(skills_created)
|
|
53
|
+
|
|
54
|
+
# 2. Generate steering file for routing
|
|
55
|
+
steering_dir = target_path / ".kiro" / "steering"
|
|
56
|
+
steering_created = self._install_steering(steering_dir, dry_run)
|
|
57
|
+
result.files_created.extend(steering_created)
|
|
58
|
+
|
|
59
|
+
# 3. Generate AGENTS.md bootstrap
|
|
60
|
+
agents_md = self._generate_agents_md(target_path, dry_run)
|
|
61
|
+
if agents_md:
|
|
62
|
+
result.files_created.append(agents_md)
|
|
63
|
+
|
|
64
|
+
if not dry_run:
|
|
65
|
+
self._update_lock_state(scope)
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
result.success = False
|
|
69
|
+
result.errors.append(str(e))
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
def _install_skills(self, skills_dir: Path, dry_run: bool) -> list[str]:
|
|
74
|
+
created = []
|
|
75
|
+
skills_root = self.skills_registry.skills_root
|
|
76
|
+
|
|
77
|
+
if not skills_root.exists():
|
|
78
|
+
return created
|
|
79
|
+
|
|
80
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
for category_dir in skills_root.iterdir():
|
|
83
|
+
if not category_dir.is_dir():
|
|
84
|
+
continue
|
|
85
|
+
for skill_dir in category_dir.iterdir():
|
|
86
|
+
if not skill_dir.is_dir():
|
|
87
|
+
continue
|
|
88
|
+
skill_file = skill_dir / "SKILL.md"
|
|
89
|
+
if not skill_file.exists():
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
target_skill_dir = skills_dir / skill_dir.name
|
|
93
|
+
if dry_run:
|
|
94
|
+
created.append(str(target_skill_dir.relative_to(self.project_root)))
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if target_skill_dir.exists():
|
|
98
|
+
shutil.rmtree(target_skill_dir)
|
|
99
|
+
shutil.copytree(skill_dir, target_skill_dir)
|
|
100
|
+
|
|
101
|
+
created.append(str(target_skill_dir.relative_to(self.project_root)))
|
|
102
|
+
|
|
103
|
+
if not dry_run:
|
|
104
|
+
self._record_file(self.target, "project", target_skill_dir / "SKILL.md")
|
|
105
|
+
|
|
106
|
+
return created
|
|
107
|
+
|
|
108
|
+
def _install_steering(self, steering_dir: Path, dry_run: bool) -> list[str]:
|
|
109
|
+
created = []
|
|
110
|
+
steering_dir.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
|
|
112
|
+
# Generate main steering file
|
|
113
|
+
steering_path = steering_dir / "python-skills.md"
|
|
114
|
+
content = self._generate_steering_content()
|
|
115
|
+
|
|
116
|
+
if dry_run:
|
|
117
|
+
created.append(str(steering_path.relative_to(self.project_root)))
|
|
118
|
+
else:
|
|
119
|
+
self._safe_write_file(steering_path, content, dry_run)
|
|
120
|
+
created.append(str(steering_path.relative_to(self.project_root)))
|
|
121
|
+
self._record_file(self.target, "project", steering_path)
|
|
122
|
+
|
|
123
|
+
return created
|
|
124
|
+
|
|
125
|
+
def _generate_steering_content(self) -> str:
|
|
126
|
+
categories = {}
|
|
127
|
+
for skill in self.skills_registry.get_all_skills():
|
|
128
|
+
if skill.category not in categories:
|
|
129
|
+
categories[skill.category] = []
|
|
130
|
+
categories[skill.category].append(skill.name)
|
|
131
|
+
|
|
132
|
+
parts = [
|
|
133
|
+
"---",
|
|
134
|
+
"inclusion: auto",
|
|
135
|
+
"name: python-skills",
|
|
136
|
+
"description: Python engineering skills and patterns",
|
|
137
|
+
"---",
|
|
138
|
+
"",
|
|
139
|
+
"<!-- BEGIN PYTHON-SKILLS MANAGED -->",
|
|
140
|
+
"",
|
|
141
|
+
"# Python Skills Integration",
|
|
142
|
+
"",
|
|
143
|
+
"This project uses [python-skills](https://github.com/FoxPink-dev/python-skills) for Python engineering standards.",
|
|
144
|
+
"",
|
|
145
|
+
"## Available Skill Categories",
|
|
146
|
+
"",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for category, skills in sorted(categories.items()):
|
|
150
|
+
parts = [f"### {category.title()}", ""]
|
|
151
|
+
for skill in sorted(skills):
|
|
152
|
+
parts.append(f"- `{skill}`")
|
|
153
|
+
parts.append("")
|
|
154
|
+
parts.extend(parts)
|
|
155
|
+
|
|
156
|
+
parts.extend([
|
|
157
|
+
"## Usage",
|
|
158
|
+
"",
|
|
159
|
+
"Reference relevant skills when working on Python code. Key patterns:",
|
|
160
|
+
"",
|
|
161
|
+
"- **Type hints**: Use built-in generics, `str | None`, `Protocol`",
|
|
162
|
+
"- **Async**: `asyncio.gather`, `Semaphore`, `TaskGroup` (3.11+)",
|
|
163
|
+
"- **HTTP**: `httpx` with retries, timeouts, connection pooling",
|
|
164
|
+
"- **Security**: Parameterized queries, `pathlib` for paths, `yaml.safe_load`",
|
|
165
|
+
"- **Testing**: `pytest` with parametrized tests, edge cases",
|
|
166
|
+
"- **CLI**: `click`/`typer`, config layering, secrets via env",
|
|
167
|
+
"",
|
|
168
|
+
"Reference: [python-skills repository](https://github.com/FoxPink-dev/python-skills)",
|
|
169
|
+
"",
|
|
170
|
+
"<!-- END PYTHON-SKILLS MANAGED -->",
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
return "\n".join(parts)
|
|
174
|
+
|
|
175
|
+
def _generate_agents_md(self, target_path: Path, dry_run: bool) -> str | None:
|
|
176
|
+
agents_md = target_path / "AGENTS.md"
|
|
177
|
+
bootstrap = self._generate_agents_md_content()
|
|
178
|
+
|
|
179
|
+
if dry_run:
|
|
180
|
+
return str(agents_md.relative_to(self.project_root))
|
|
181
|
+
|
|
182
|
+
if agents_md.exists():
|
|
183
|
+
existing = agents_md.read_text(encoding="utf-8")
|
|
184
|
+
new_content, replaced = self._replace_managed_region(existing, bootstrap, agents_md)
|
|
185
|
+
if replaced:
|
|
186
|
+
self._safe_write_file(agents_md, new_content, dry_run)
|
|
187
|
+
self._record_file(self.target, "project", agents_md, (BEGIN_MARKER, END_MARKER))
|
|
188
|
+
else:
|
|
189
|
+
new_content = existing + "\n\n" + self._wrap_managed(bootstrap, agents_md)
|
|
190
|
+
self._safe_write_file(agents_md, new_content, dry_run)
|
|
191
|
+
self._record_file(self.target, "project", agents_md, (BEGIN_MARKER, END_MARKER))
|
|
192
|
+
else:
|
|
193
|
+
self._safe_write_file(agents_md, self._wrap_managed(bootstrap, agents_md), dry_run)
|
|
194
|
+
self._record_file(self.target, "project", agents_md, (BEGIN_MARKER, END_MARKER))
|
|
195
|
+
|
|
196
|
+
return str(agents_md.relative_to(self.project_root))
|
|
197
|
+
|
|
198
|
+
def _generate_agents_md_content(self) -> str:
|
|
199
|
+
return """<!-- BEGIN PYTHON-SKILLS MANAGED -->
|
|
200
|
+
# Python Skills Integration
|
|
201
|
+
|
|
202
|
+
This project uses [python-skills](https://github.com/FoxPink-dev/python-skills) for Python engineering standards.
|
|
203
|
+
|
|
204
|
+
## Available Skills
|
|
205
|
+
|
|
206
|
+
The canonical skill library is at `python-skills/skills/` with 69 skills across 10 categories.
|
|
207
|
+
|
|
208
|
+
### Core
|
|
209
|
+
- variables_types, control_flow, functions, data_structures, oop, comprehensions, advanced_python
|
|
210
|
+
|
|
211
|
+
### Stdlib
|
|
212
|
+
- argparse, collections, datetime, functools, itertools, json, logging, os_sys, pathlib, re, statistics, subprocess
|
|
213
|
+
|
|
214
|
+
### Generation
|
|
215
|
+
- type_hints, protocols_generics, async_concurrency, error_handling, validation_pipeline, workflow
|
|
216
|
+
|
|
217
|
+
### Engineering
|
|
218
|
+
- cli_apps, configuration, database, dependency_management, http_clients, logging, modules_packages, packaging, project_structure, pyproject_toml, virtual_environments
|
|
219
|
+
|
|
220
|
+
### Quality
|
|
221
|
+
- abstractions, comments, documentation, duplication, functions, maintainability, naming, readability, type_annotations
|
|
222
|
+
|
|
223
|
+
### Security
|
|
224
|
+
- auth_boundaries, command_injection, dependency_risks, file_handling, input_validation, path_traversal, secrets, sql_injection, unsafe_deserialization
|
|
225
|
+
|
|
226
|
+
### Testing
|
|
227
|
+
- async_tests, coverage, edge_cases, fixtures_mocks, organization, parameterized, regression_tests
|
|
228
|
+
|
|
229
|
+
### Refactoring
|
|
230
|
+
- behavior_preservation, incremental, interface_stability, safe_refactoring
|
|
231
|
+
|
|
232
|
+
### Debugging
|
|
233
|
+
- common_bugs, inspection_techniques, root_cause
|
|
234
|
+
|
|
235
|
+
### Anti-Patterns
|
|
236
|
+
- index
|
|
237
|
+
|
|
238
|
+
## Usage
|
|
239
|
+
Reference relevant skills when working on Python code. Skills are loaded on-demand based on task context.
|
|
240
|
+
<!-- END PYTHON-SKILLS MANAGED -->"""
|
|
241
|
+
|
|
242
|
+
def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
|
|
243
|
+
result = SyncResult(success=True, target=self.target, scope=scope)
|
|
244
|
+
target_path = self._get_scope_path(scope)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
skills_root = self.skills_registry.skills_root
|
|
248
|
+
|
|
249
|
+
# Sync skills
|
|
250
|
+
skills_dir = target_path / ".kiro" / "skills"
|
|
251
|
+
if skills_dir.exists():
|
|
252
|
+
for skill_dir in skills_dir.iterdir():
|
|
253
|
+
if skill_dir.is_dir():
|
|
254
|
+
canonical = skills_root / skill_dir.name
|
|
255
|
+
if canonical.exists():
|
|
256
|
+
# Check if changed (could use hash comparison)
|
|
257
|
+
pass
|
|
258
|
+
else:
|
|
259
|
+
if not dry_run:
|
|
260
|
+
shutil.rmtree(skill_dir)
|
|
261
|
+
result.removed.append(str(skill_dir.relative_to(self.project_root)))
|
|
262
|
+
|
|
263
|
+
# Add new skills
|
|
264
|
+
for category_dir in skills_root.iterdir():
|
|
265
|
+
if not category_dir.is_dir():
|
|
266
|
+
continue
|
|
267
|
+
for skill_dir in category_dir.iterdir():
|
|
268
|
+
if not skill_dir.is_dir():
|
|
269
|
+
continue
|
|
270
|
+
target_skill_dir = skills_dir / skill_dir.name
|
|
271
|
+
if not target_skill_dir.exists():
|
|
272
|
+
if not dry_run:
|
|
273
|
+
shutil.copytree(skill_dir, target_skill_dir)
|
|
274
|
+
result.added.append(str(target_skill_dir.relative_to(self.project_root)))
|
|
275
|
+
|
|
276
|
+
# Sync steering
|
|
277
|
+
steering_dir = target_path / ".kiro" / "steering"
|
|
278
|
+
steering_file = steering_dir / "python-skills.md"
|
|
279
|
+
if steering_file.exists():
|
|
280
|
+
current = steering_file.read_text(encoding="utf-8")
|
|
281
|
+
new_content = self._generate_steering_content()
|
|
282
|
+
if current.strip() != new_content.strip():
|
|
283
|
+
if not dry_run:
|
|
284
|
+
self._safe_write_file(steering_file, new_content, dry_run)
|
|
285
|
+
result.modified.append(str(steering_file.relative_to(self.project_root)))
|
|
286
|
+
|
|
287
|
+
# Sync AGENTS.md
|
|
288
|
+
agents_md = target_path / "AGENTS.md"
|
|
289
|
+
if agents_md.exists():
|
|
290
|
+
current = agents_md.read_text(encoding="utf-8")
|
|
291
|
+
new_agents = self._generate_agents_md_content()
|
|
292
|
+
new_content, replaced = self._replace_managed_region(current, new_agents, agents_md)
|
|
293
|
+
if replaced and not dry_run:
|
|
294
|
+
self._safe_write_file(agents_md, new_content, dry_run)
|
|
295
|
+
result.modified.append(str(agents_md.relative_to(self.project_root)))
|
|
296
|
+
|
|
297
|
+
if not dry_run:
|
|
298
|
+
self._update_lock_state(scope)
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
result.success = False
|
|
302
|
+
result.errors.append(str(e))
|
|
303
|
+
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
|
|
307
|
+
result = UninstallResult(success=True, target=self.target, scope=scope)
|
|
308
|
+
target_path = self._get_scope_path(scope)
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
# Remove skills
|
|
312
|
+
skills_dir = target_path / ".kiro" / "skills"
|
|
313
|
+
if skills_dir.exists():
|
|
314
|
+
if not dry_run:
|
|
315
|
+
shutil.rmtree(skills_dir)
|
|
316
|
+
result.files_removed.append(str(skills_dir.relative_to(self.project_root)))
|
|
317
|
+
|
|
318
|
+
# Remove steering
|
|
319
|
+
steering_dir = target_path / ".kiro" / "steering"
|
|
320
|
+
steering_file = steering_dir / "python-skills.md"
|
|
321
|
+
if steering_file.exists() and self._has_ownership_marker(steering_file):
|
|
322
|
+
if not dry_run:
|
|
323
|
+
steering_file.unlink()
|
|
324
|
+
result.files_removed.append(str(steering_file.relative_to(self.project_root)))
|
|
325
|
+
|
|
326
|
+
# Remove AGENTS.md section
|
|
327
|
+
agents_md = target_path / "AGENTS.md"
|
|
328
|
+
if agents_md.exists():
|
|
329
|
+
content = agents_md.read_text(encoding="utf-8")
|
|
330
|
+
new_content, removed = self._remove_managed_region(content, agents_md)
|
|
331
|
+
if removed and not dry_run:
|
|
332
|
+
self._safe_write_file(agents_md, new_content, dry_run)
|
|
333
|
+
result.regions_removed.append(str(agents_md.relative_to(self.project_root)))
|
|
334
|
+
|
|
335
|
+
if not dry_run:
|
|
336
|
+
self._update_lock_state(scope)
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
result.success = False
|
|
340
|
+
result.errors.append(str(e))
|
|
341
|
+
|
|
342
|
+
return result
|
|
343
|
+
|
|
344
|
+
def status(self, scope: str = "project") -> StatusResult:
|
|
345
|
+
target_path = self._get_scope_path(scope)
|
|
346
|
+
installed = False
|
|
347
|
+
files = []
|
|
348
|
+
|
|
349
|
+
skills_dir = target_path / ".kiro" / "skills"
|
|
350
|
+
if skills_dir.exists():
|
|
351
|
+
installed = True
|
|
352
|
+
for f in skills_dir.rglob("SKILL.md"):
|
|
353
|
+
files.append(str(f.relative_to(self.project_root)))
|
|
354
|
+
|
|
355
|
+
steering_file = target_path / ".kiro" / "steering" / "python-skills.md"
|
|
356
|
+
if steering_file.exists() and self._has_ownership_marker(steering_file):
|
|
357
|
+
installed = True
|
|
358
|
+
files.append(str(steering_file.relative_to(self.project_root)))
|
|
359
|
+
|
|
360
|
+
agents_md = target_path / "AGENTS.md"
|
|
361
|
+
if agents_md.exists() and self._has_ownership_marker(agents_md):
|
|
362
|
+
installed = True
|
|
363
|
+
files.append(str(agents_md.relative_to(self.project_root)))
|
|
364
|
+
|
|
365
|
+
return StatusResult(
|
|
366
|
+
target=self.target,
|
|
367
|
+
scope=scope,
|
|
368
|
+
installed=installed,
|
|
369
|
+
files=files,
|
|
370
|
+
version="1.0.0"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def _has_ownership_marker(self, path: Path) -> bool:
|
|
374
|
+
"""Check if a file has our ownership markers."""
|
|
375
|
+
if not path.exists():
|
|
376
|
+
return False
|
|
377
|
+
content = path.read_text(encoding="utf-8")
|
|
378
|
+
begin, end = self._get_markers(path)
|
|
379
|
+
return begin in content and end in content
|
|
380
|
+
|
|
381
|
+
def _update_lock_state(self, scope: str) -> None:
|
|
382
|
+
self.lock_manager.update_canonical_hash(self.skills_registry.skills_root)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""OpenCode adapter for python-skills."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .agent_skills import AgentSkillsAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpenCodeAdapter(AgentSkillsAdapter):
|
|
9
|
+
"""Adapter for OpenCode (CLI/TUI/Desktop).
|
|
10
|
+
|
|
11
|
+
OpenCode discovers skills from:
|
|
12
|
+
- .agents/skills/<name>/SKILL.md (cross-agent standard)
|
|
13
|
+
- .opencode/skills/<name>/SKILL.md (native)
|
|
14
|
+
- .claude/skills/<name>/SKILL.md (Claude Code compat)
|
|
15
|
+
|
|
16
|
+
We install to .agents/skills/ (shared) and .opencode/skills/ (native).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
target_name = "opencode"
|
|
20
|
+
display_name = "OpenCode"
|
|
21
|
+
agent_skills_dir = ".agents/skills"
|
|
22
|
+
native_skills_dir = ".opencode/skills"
|
|
23
|
+
detection_app_command = "opencode"
|
|
24
|
+
detection_project_markers = [".opencode", "opencode.json", "opencode.jsonc"]
|
|
25
|
+
|
|
26
|
+
def _get_global_path(self) -> Path:
|
|
27
|
+
return Path.home() / ".config" / "opencode" / "skills"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Roo Code adapter for python-skills."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .agent_skills import AgentSkillsAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RooAdapter(AgentSkillsAdapter):
|
|
9
|
+
"""Adapter for Roo Code (formerly Roo Cline).
|
|
10
|
+
|
|
11
|
+
Roo Code discovers skills from:
|
|
12
|
+
- .roo/skills/<name>/SKILL.md (native)
|
|
13
|
+
- .agents/skills/<name>/SKILL.md (cross-agent standard)
|
|
14
|
+
|
|
15
|
+
We install to .agents/skills/ (shared) and .roo/skills/ (native).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
target_name = "roo"
|
|
19
|
+
display_name = "Roo Code"
|
|
20
|
+
agent_skills_dir = ".agents/skills"
|
|
21
|
+
native_skills_dir = ".roo/skills"
|
|
22
|
+
detection_project_markers = [".roo", ".roorules"]
|
|
23
|
+
|
|
24
|
+
def _get_global_path(self) -> Path:
|
|
25
|
+
return Path.home() / ".roo" / "skills"
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Universal AGENTS.md adapter for python-skills."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..adapters.base import (
|
|
6
|
+
AgentAdapter,
|
|
7
|
+
DetectionResult,
|
|
8
|
+
InstallResult,
|
|
9
|
+
StatusResult,
|
|
10
|
+
SyncResult,
|
|
11
|
+
UninstallResult,
|
|
12
|
+
)
|
|
13
|
+
from ..config import Target
|
|
14
|
+
from ..markers import BEGIN_MARKER, END_MARKER
|
|
15
|
+
from ..skills.registry import SkillRegistry
|
|
16
|
+
from ..state import LockManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UniversalAdapter(AgentAdapter):
|
|
20
|
+
"""Adapter for Universal AGENTS.md integration."""
|
|
21
|
+
|
|
22
|
+
name = "Universal AGENTS.md"
|
|
23
|
+
target = Target.UNIVERSAL.value
|
|
24
|
+
|
|
25
|
+
def __init__(self, project_root: Path, skills_registry: SkillRegistry, lock_manager: LockManager, config):
|
|
26
|
+
super().__init__(project_root, skills_registry, lock_manager, config)
|
|
27
|
+
|
|
28
|
+
def _get_global_path(self) -> Path:
|
|
29
|
+
return Path.home() / ".agents"
|
|
30
|
+
|
|
31
|
+
def detect(self) -> DetectionResult:
|
|
32
|
+
# AGENTS.md is universally supported - no app detection needed
|
|
33
|
+
project_config = (self.project_root / "AGENTS.md").exists()
|
|
34
|
+
global_config = self._get_global_path().exists()
|
|
35
|
+
|
|
36
|
+
return DetectionResult(
|
|
37
|
+
application_detected=True, # Always available
|
|
38
|
+
project_config_detected=project_config,
|
|
39
|
+
adapter_available=True,
|
|
40
|
+
details=f"Universal adapter always available. Project AGENTS.md: {'yes' if project_config else 'no'}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
|
|
44
|
+
result = InstallResult(success=True, target=self.target, scope=scope)
|
|
45
|
+
target_path = self._get_scope_path(scope)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
agents_md = target_path / "AGENTS.md"
|
|
49
|
+
content = self._generate_agents_md_content()
|
|
50
|
+
|
|
51
|
+
if dry_run:
|
|
52
|
+
result.files_created.append(str(agents_md.relative_to(self.project_root)))
|
|
53
|
+
else:
|
|
54
|
+
if agents_md.exists():
|
|
55
|
+
existing = agents_md.read_text(encoding="utf-8")
|
|
56
|
+
new_content, replaced = self._replace_managed_region(existing, content, agents_md)
|
|
57
|
+
if replaced:
|
|
58
|
+
self._safe_write_file(agents_md, new_content, dry_run)
|
|
59
|
+
else:
|
|
60
|
+
new_content = existing + "\n\n" + self._wrap_managed(content, agents_md)
|
|
61
|
+
self._safe_write_file(agents_md, new_content, dry_run)
|
|
62
|
+
else:
|
|
63
|
+
self._safe_write_file(agents_md, self._wrap_managed(content, agents_md), dry_run)
|
|
64
|
+
|
|
65
|
+
result.files_created.append(str(agents_md.relative_to(self.project_root)))
|
|
66
|
+
self._record_file(self.target, scope, agents_md, (BEGIN_MARKER, END_MARKER))
|
|
67
|
+
|
|
68
|
+
if not dry_run:
|
|
69
|
+
self._update_lock_state(scope)
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
result.success = False
|
|
73
|
+
result.errors.append(str(e))
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
def _generate_agents_md_content(self) -> str:
|
|
78
|
+
categories = {}
|
|
79
|
+
for skill in self.skills_registry.get_all_skills():
|
|
80
|
+
if skill.category not in categories:
|
|
81
|
+
categories[skill.category] = []
|
|
82
|
+
categories[skill.category].append(skill.name)
|
|
83
|
+
|
|
84
|
+
parts = [
|
|
85
|
+
"<!-- BEGIN PYTHON-SKILLS MANAGED -->",
|
|
86
|
+
"",
|
|
87
|
+
"# Python Skills Integration",
|
|
88
|
+
"",
|
|
89
|
+
"This project uses [python-skills](https://github.com/FoxPink-dev/python-skills) for Python engineering standards.",
|
|
90
|
+
"",
|
|
91
|
+
"## Available Skills",
|
|
92
|
+
"",
|
|
93
|
+
"The canonical skill library is at `python-skills/skills/` with 69 skills across 10 categories.",
|
|
94
|
+
"",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
for category, skills in sorted(categories.items()):
|
|
98
|
+
cat_parts = [f"### {category.title()}", ""]
|
|
99
|
+
for skill in sorted(skills):
|
|
100
|
+
cat_parts.append(f"- `{skill}`")
|
|
101
|
+
cat_parts.append("")
|
|
102
|
+
parts.extend(cat_parts)
|
|
103
|
+
|
|
104
|
+
parts.extend([
|
|
105
|
+
"## Usage",
|
|
106
|
+
"",
|
|
107
|
+
"Reference relevant skills when working on Python code. Skills are loaded on-demand based on task context.",
|
|
108
|
+
"",
|
|
109
|
+
"## Reference",
|
|
110
|
+
"",
|
|
111
|
+
"- Repository: https://github.com/FoxPink-dev/python-skills",
|
|
112
|
+
"- Canonical skills location: `python-skills/skills/` (in python-skills package)",
|
|
113
|
+
"",
|
|
114
|
+
"<!-- END PYTHON-SKILLS MANAGED -->",
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
return "\n".join(parts)
|
|
118
|
+
|
|
119
|
+
def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
|
|
120
|
+
result = SyncResult(success=True, target=self.target, scope=scope)
|
|
121
|
+
target_path = self._get_scope_path(scope)
|
|
122
|
+
agents_md = target_path / "AGENTS.md"
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
if not agents_md.exists():
|
|
126
|
+
result.success = False
|
|
127
|
+
result.errors.append("AGENTS.md not found")
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
current = agents_md.read_text(encoding="utf-8")
|
|
131
|
+
new_content = self._generate_agents_md_content()
|
|
132
|
+
|
|
133
|
+
# Extract current managed region
|
|
134
|
+
current_managed = self._extract_managed_region(current, agents_md)
|
|
135
|
+
new_managed = self._extract_managed_region(new_content, agents_md)
|
|
136
|
+
|
|
137
|
+
if current_managed and new_managed and current_managed.strip() != new_managed.strip():
|
|
138
|
+
new_content, replaced = self._replace_managed_region(current, new_content, agents_md)
|
|
139
|
+
if replaced and not dry_run:
|
|
140
|
+
self._safe_write_file(agents_md, new_content, dry_run)
|
|
141
|
+
result.modified.append(str(agents_md.relative_to(self.project_root)))
|
|
142
|
+
else:
|
|
143
|
+
result.skipped.append(str(agents_md.relative_to(self.project_root)))
|
|
144
|
+
|
|
145
|
+
if not dry_run:
|
|
146
|
+
self._update_lock_state(scope)
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
result.success = False
|
|
150
|
+
result.errors.append(str(e))
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
|
|
155
|
+
result = UninstallResult(success=True, target=self.target, scope=scope)
|
|
156
|
+
target_path = self._get_scope_path(scope)
|
|
157
|
+
agents_md = target_path / "AGENTS.md"
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
if agents_md.exists():
|
|
161
|
+
content = agents_md.read_text(encoding="utf-8")
|
|
162
|
+
new_content, removed = self._remove_managed_region(content, agents_md)
|
|
163
|
+
if removed and not dry_run:
|
|
164
|
+
self._safe_write_file(agents_md, new_content, dry_run)
|
|
165
|
+
result.regions_removed.append(str(agents_md.relative_to(self.project_root)))
|
|
166
|
+
|
|
167
|
+
if not dry_run:
|
|
168
|
+
self._update_lock_state(scope)
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
result.success = False
|
|
172
|
+
result.errors.append(str(e))
|
|
173
|
+
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
def status(self, scope: str = "project") -> StatusResult:
|
|
177
|
+
target_path = self._get_scope_path(scope)
|
|
178
|
+
installed = False
|
|
179
|
+
files = []
|
|
180
|
+
|
|
181
|
+
agents_md = target_path / "AGENTS.md"
|
|
182
|
+
if agents_md.exists() and self._has_ownership_marker(agents_md):
|
|
183
|
+
installed = True
|
|
184
|
+
files.append(str(agents_md.relative_to(self.project_root)))
|
|
185
|
+
|
|
186
|
+
return StatusResult(
|
|
187
|
+
target=self.target,
|
|
188
|
+
scope=scope,
|
|
189
|
+
installed=installed,
|
|
190
|
+
files=files,
|
|
191
|
+
version="1.0.0"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _has_ownership_marker(self, path: Path) -> bool:
|
|
195
|
+
"""Check if a file has our ownership markers."""
|
|
196
|
+
if not path.exists():
|
|
197
|
+
return False
|
|
198
|
+
content = path.read_text(encoding="utf-8")
|
|
199
|
+
begin, end = self._get_markers(path)
|
|
200
|
+
return begin in content and end in content
|
|
201
|
+
|
|
202
|
+
def _update_lock_state(self, scope: str) -> None:
|
|
203
|
+
self.lock_manager.update_canonical_hash(self.skills_registry.skills_root)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""VS Code / GitHub Copilot adapter for python-skills."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .agent_skills import AgentSkillsAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VSCodeAdapter(AgentSkillsAdapter):
|
|
9
|
+
"""Adapter for VS Code / GitHub Copilot.
|
|
10
|
+
|
|
11
|
+
VS Code / Copilot discovers skills from:
|
|
12
|
+
- .agents/skills/<name>/SKILL.md (cross-agent standard)
|
|
13
|
+
- .github/skills/<name>/SKILL.md (native)
|
|
14
|
+
- .claude/skills/<name>/SKILL.md (Claude Code compat)
|
|
15
|
+
|
|
16
|
+
We install to .agents/skills/ (shared) and .github/skills/ (native).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
target_name = "vscode"
|
|
20
|
+
display_name = "VS Code / GitHub Copilot"
|
|
21
|
+
agent_skills_dir = ".agents/skills"
|
|
22
|
+
native_skills_dir = ".github/skills"
|
|
23
|
+
detection_app_command = "code"
|
|
24
|
+
detection_project_markers = [".github", ".vscode"]
|
|
25
|
+
|
|
26
|
+
def _get_global_path(self) -> Path:
|
|
27
|
+
return Path.home() / ".copilot" / "skills"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Windsurf adapter for python-skills."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .agent_skills import AgentSkillsAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WindsurfAdapter(AgentSkillsAdapter):
|
|
9
|
+
"""Adapter for Windsurf (Devin Desktop/Cascade).
|
|
10
|
+
|
|
11
|
+
Windsurf discovers skills from:
|
|
12
|
+
- .agents/skills/<name>/SKILL.md (cross-agent standard)
|
|
13
|
+
- .windsurf/skills/<name>/SKILL.md (native)
|
|
14
|
+
- .claude/skills/<name>/SKILL.md (Claude Code compat)
|
|
15
|
+
|
|
16
|
+
We install to .agents/skills/ (shared) and .windsurf/skills/ (native).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
target_name = "windsurf"
|
|
20
|
+
display_name = "Windsurf"
|
|
21
|
+
agent_skills_dir = ".agents/skills"
|
|
22
|
+
native_skills_dir = ".windsurf/skills"
|
|
23
|
+
detection_project_markers = [".windsurf", ".devin"]
|
|
24
|
+
|
|
25
|
+
def _get_global_path(self) -> Path:
|
|
26
|
+
return Path.home() / ".codeium" / "windsurf" / "skills"
|