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.
Files changed (105) hide show
  1. python_skills/__init__.py +10 -0
  2. python_skills/__main__.py +6 -0
  3. python_skills/adapters/__init__.py +48 -0
  4. python_skills/adapters/agent_skills.py +415 -0
  5. python_skills/adapters/aider_adapter.py +226 -0
  6. python_skills/adapters/base.py +153 -0
  7. python_skills/adapters/claude.py +474 -0
  8. python_skills/adapters/cline.py +332 -0
  9. python_skills/adapters/codex.py +24 -0
  10. python_skills/adapters/continue_adapter.py +198 -0
  11. python_skills/adapters/cursor.py +327 -0
  12. python_skills/adapters/gemini.py +26 -0
  13. python_skills/adapters/goose.py +26 -0
  14. python_skills/adapters/junie.py +25 -0
  15. python_skills/adapters/kiro.py +382 -0
  16. python_skills/adapters/opencode.py +27 -0
  17. python_skills/adapters/roo.py +25 -0
  18. python_skills/adapters/universal.py +203 -0
  19. python_skills/adapters/vscode.py +27 -0
  20. python_skills/adapters/windsurf.py +26 -0
  21. python_skills/adapters/zed.py +27 -0
  22. python_skills/cli.py +326 -0
  23. python_skills/config.py +160 -0
  24. python_skills/detector.py +152 -0
  25. python_skills/installer.py +163 -0
  26. python_skills/markers.py +115 -0
  27. python_skills/skills/__init__.py +14 -0
  28. python_skills/skills/loader.py +171 -0
  29. python_skills/skills/metadata.py +152 -0
  30. python_skills/skills/registry.py +101 -0
  31. python_skills/state.py +204 -0
  32. python_skills-1.0.0.dist-info/METADATA +99 -0
  33. python_skills-1.0.0.dist-info/RECORD +105 -0
  34. python_skills-1.0.0.dist-info/WHEEL +4 -0
  35. python_skills-1.0.0.dist-info/entry_points.txt +2 -0
  36. python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
  37. skills/advanced_python.md +239 -0
  38. skills/anti_patterns/index.md +406 -0
  39. skills/comprehensions.md +167 -0
  40. skills/control_flow.md +175 -0
  41. skills/data_structures.md +243 -0
  42. skills/debugging/common_bugs.md +222 -0
  43. skills/debugging/inspection_techniques.md +249 -0
  44. skills/debugging/root_cause.md +203 -0
  45. skills/engineering/application_logging.md +195 -0
  46. skills/engineering/cli_apps.md +207 -0
  47. skills/engineering/configuration.md +218 -0
  48. skills/engineering/database.md +240 -0
  49. skills/engineering/dependency_management.md +205 -0
  50. skills/engineering/http_clients.md +267 -0
  51. skills/engineering/modules_packages.md +211 -0
  52. skills/engineering/packaging.md +197 -0
  53. skills/engineering/project_structure.md +155 -0
  54. skills/engineering/pyproject_toml.md +302 -0
  55. skills/engineering/virtual_environments.md +206 -0
  56. skills/functions.md +244 -0
  57. skills/generation/async_concurrency.md +291 -0
  58. skills/generation/error_handling.md +276 -0
  59. skills/generation/protocols_generics.md +243 -0
  60. skills/generation/type_hints.md +290 -0
  61. skills/generation/validation_pipeline.md +274 -0
  62. skills/generation/workflow.md +190 -0
  63. skills/oop.md +228 -0
  64. skills/quality/abstractions.md +154 -0
  65. skills/quality/comments.md +177 -0
  66. skills/quality/documentation.md +176 -0
  67. skills/quality/duplication.md +137 -0
  68. skills/quality/maintainability.md +142 -0
  69. skills/quality/naming.md +171 -0
  70. skills/quality/quality_functions.md +245 -0
  71. skills/quality/readability.md +239 -0
  72. skills/quality/type_annotations.md +192 -0
  73. skills/refactoring/behavior_preservation.md +157 -0
  74. skills/refactoring/incremental.md +187 -0
  75. skills/refactoring/interface_stability.md +199 -0
  76. skills/refactoring/safe_refactoring.md +206 -0
  77. skills/security/auth_boundaries.md +200 -0
  78. skills/security/command_injection.md +207 -0
  79. skills/security/dependency_risks.md +282 -0
  80. skills/security/file_handling.md +156 -0
  81. skills/security/input_validation.md +190 -0
  82. skills/security/path_traversal.md +172 -0
  83. skills/security/secrets.md +171 -0
  84. skills/security/sql_injection.md +188 -0
  85. skills/security/unsafe_deserialization.md +164 -0
  86. skills/stdlib/argparse.md +178 -0
  87. skills/stdlib/collections.md +212 -0
  88. skills/stdlib/datetime.md +187 -0
  89. skills/stdlib/functools.md +238 -0
  90. skills/stdlib/itertools.md +183 -0
  91. skills/stdlib/json.md +162 -0
  92. skills/stdlib/logging.md +185 -0
  93. skills/stdlib/os_sys.md +184 -0
  94. skills/stdlib/pathlib.md +218 -0
  95. skills/stdlib/re.md +171 -0
  96. skills/stdlib/statistics.md +112 -0
  97. skills/stdlib/subprocess.md +211 -0
  98. skills/testing/async_tests.md +249 -0
  99. skills/testing/coverage.md +168 -0
  100. skills/testing/edge_cases.md +197 -0
  101. skills/testing/fixtures_mocks.md +203 -0
  102. skills/testing/organization.md +205 -0
  103. skills/testing/parameterized.md +174 -0
  104. skills/testing/regression_tests.md +165 -0
  105. 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"