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,332 @@
1
+ """Cline adapter for python-skills.
2
+
3
+ Cline supports both:
4
+ 1. .clinerules/ (rules-based, legacy)
5
+ 2. .cline/skills/<name>/SKILL.md (Agent Skills, native)
6
+ 3. .agents/skills/<name>/SKILL.md (cross-agent standard)
7
+
8
+ This adapter installs both rules AND skills for maximum compatibility.
9
+ """
10
+
11
+ import shutil
12
+ from pathlib import Path
13
+
14
+ from ..adapters.base import (
15
+ AgentAdapter,
16
+ DetectionResult,
17
+ InstallResult,
18
+ StatusResult,
19
+ SyncResult,
20
+ UninstallResult,
21
+ )
22
+ from ..config import Target
23
+ from ..skills.registry import SkillRegistry
24
+ from ..state import LockManager
25
+
26
+
27
+ class ClineAdapter(AgentAdapter):
28
+ """Adapter for Cline.
29
+
30
+ Installs:
31
+ - .agents/skills/<name>/SKILL.md (Agent Skills)
32
+ - .cline/skills/<name>/SKILL.md (native)
33
+ - .clinerules/python-skills.md (rules, backward compat)
34
+ """
35
+
36
+ name = "Cline"
37
+ target = Target.CLINE.value
38
+
39
+ def __init__(self, project_root: Path, skills_registry: SkillRegistry, lock_manager: LockManager, config):
40
+ super().__init__(project_root, skills_registry, lock_manager, config)
41
+
42
+ def _get_global_path(self) -> Path:
43
+ import platform
44
+ system = platform.system()
45
+ if system == "Windows":
46
+ return Path.home() / "Documents" / "Cline" / "Rules"
47
+ else:
48
+ return Path.home() / "Documents" / "Cline" / "Rules"
49
+
50
+ def detect(self) -> DetectionResult:
51
+ app_detected = False
52
+ try:
53
+ import subprocess
54
+ result = subprocess.run(["code", "--list-extensions"], capture_output=True, text=True)
55
+ if "cline.cline" in result.stdout.lower():
56
+ app_detected = True
57
+ except Exception:
58
+ pass
59
+
60
+ project_config = (
61
+ (self.project_root / ".clinerules").exists() or
62
+ (self.project_root / ".cline").exists() or
63
+ (self.project_root / ".agents" / "skills").exists()
64
+ )
65
+ global_config = self._get_global_path().exists()
66
+
67
+ return DetectionResult(
68
+ application_detected=app_detected,
69
+ project_config_detected=project_config,
70
+ adapter_available=True,
71
+ details=f"App: {'yes' if app_detected else 'no'}, Project: {'yes' if project_config else 'no'}, Global: {'yes' if global_config else 'no'}"
72
+ )
73
+
74
+ def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
75
+ result = InstallResult(success=True, target=self.target, scope=scope)
76
+ target_path = self._get_scope_path(scope)
77
+
78
+ try:
79
+ # 1. Install Agent Skills to .agents/skills/
80
+ agents_skills_dir = target_path / ".agents" / "skills"
81
+ agents_created = self._install_skills_to_dir(agents_skills_dir, dry_run)
82
+ result.files_created.extend(agents_created)
83
+
84
+ # 2. Install Agent Skills to .cline/skills/ (native)
85
+ cline_skills_dir = target_path / ".cline" / "skills"
86
+ cline_created = self._install_skills_to_dir(cline_skills_dir, dry_run)
87
+ result.files_created.extend(cline_created)
88
+
89
+ # 3. Generate rules file (backward compat)
90
+ rules_dir = target_path / ".clinerules"
91
+ rules_dir.mkdir(parents=True, exist_ok=True)
92
+
93
+ rule_path = rules_dir / "python-skills.md"
94
+ content = self._generate_cline_rule()
95
+
96
+ if dry_run:
97
+ result.files_created.append(str(rule_path.relative_to(self.project_root)))
98
+ else:
99
+ self._safe_write_file(rule_path, content, dry_run)
100
+ result.files_created.append(str(rule_path.relative_to(self.project_root)))
101
+ self._record_file(self.target, scope, rule_path)
102
+
103
+ if not dry_run:
104
+ self._update_lock_state(scope)
105
+
106
+ except Exception as e:
107
+ result.success = False
108
+ result.errors.append(str(e))
109
+
110
+ return result
111
+
112
+ def _install_skills_to_dir(self, skills_dir: Path, dry_run: bool) -> list[str]:
113
+ """Install canonical skills as Agent Skills SKILL.md files."""
114
+ from .agent_skills import _generate_skill_md, _skill_name_to_slug
115
+ created = []
116
+
117
+ if not self.skills_registry:
118
+ return created
119
+
120
+ skills_dir.mkdir(parents=True, exist_ok=True)
121
+
122
+ for skill in self.skills_registry.get_all_skills():
123
+ slug = _skill_name_to_slug(skill.name)
124
+ target_skill_dir = skills_dir / slug
125
+ target_skill_file = target_skill_dir / "SKILL.md"
126
+
127
+ if dry_run:
128
+ created.append(str(target_skill_file.relative_to(self.project_root)))
129
+ continue
130
+
131
+ content = self.skills_registry.get_skill_content(skill.name)
132
+ if not content:
133
+ continue
134
+
135
+ skill_md = _generate_skill_md(skill, content)
136
+
137
+ target_skill_dir.mkdir(parents=True, exist_ok=True)
138
+ target_skill_file.write_text(skill_md, encoding="utf-8")
139
+
140
+ created.append(str(target_skill_file.relative_to(self.project_root)))
141
+ self._record_file(self.target, "project", target_skill_file)
142
+
143
+ return created
144
+
145
+ def _generate_cline_rule(self) -> str:
146
+ categories = {}
147
+ for skill in self.skills_registry.get_all_skills():
148
+ if skill.category not in categories:
149
+ categories[skill.category] = []
150
+ categories[skill.category].append(skill.name)
151
+
152
+ parts = [
153
+ "---",
154
+ "paths:",
155
+ ' - "**/*.py"',
156
+ "---",
157
+ "",
158
+ "<!-- BEGIN PYTHON-SKILLS MANAGED -->",
159
+ "",
160
+ "# Python Skills Integration",
161
+ "",
162
+ "This project uses python-skills for Python engineering standards.",
163
+ "",
164
+ "## Available Skill Categories",
165
+ "",
166
+ ]
167
+
168
+ for category, skills in sorted(categories.items()):
169
+ parts.append(f"### {category.title()}")
170
+ parts.append("")
171
+ for skill in sorted(skills):
172
+ parts.append(f"- `{skill}`")
173
+ parts.append("")
174
+
175
+ parts.extend([
176
+ "## Usage",
177
+ "",
178
+ "Reference relevant skills when working on Python code.",
179
+ "",
180
+ "<!-- END PYTHON-SKILLS MANAGED -->",
181
+ ])
182
+
183
+ return "\n".join(parts)
184
+
185
+ def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
186
+ result = SyncResult(success=True, target=self.target, scope=scope)
187
+ target_path = self._get_scope_path(scope)
188
+
189
+ try:
190
+ # Sync skills in .agents/skills/
191
+ agents_skills_dir = target_path / ".agents" / "skills"
192
+ if agents_skills_dir.exists():
193
+ for item in agents_skills_dir.iterdir():
194
+ if item.is_dir() and (item / "SKILL.md").exists():
195
+ if not dry_run:
196
+ shutil.rmtree(item)
197
+ result.removed.append(str(item.relative_to(self.project_root)))
198
+
199
+ # Re-install skills
200
+ agents_created = self._install_skills_to_dir(agents_skills_dir, dry_run)
201
+ result.added.extend(agents_created)
202
+
203
+ cline_skills_dir = target_path / ".cline" / "skills"
204
+ if cline_skills_dir.exists():
205
+ for item in cline_skills_dir.iterdir():
206
+ if item.is_dir() and (item / "SKILL.md").exists():
207
+ if not dry_run:
208
+ shutil.rmtree(item)
209
+ result.removed.append(str(item.relative_to(self.project_root)))
210
+
211
+ cline_created = self._install_skills_to_dir(cline_skills_dir, dry_run)
212
+ result.added.extend(cline_created)
213
+
214
+ # Sync rules
215
+ rule_path = target_path / ".clinerules" / "python-skills.md"
216
+ if rule_path.exists():
217
+ current = rule_path.read_text(encoding="utf-8")
218
+ new_content = self._generate_cline_rule()
219
+ if current.strip() != new_content.strip():
220
+ if not dry_run:
221
+ self._safe_write_file(rule_path, new_content, dry_run)
222
+ result.modified.append(str(rule_path.relative_to(self.project_root)))
223
+
224
+ if not dry_run:
225
+ self._update_lock_state(scope)
226
+
227
+ except Exception as e:
228
+ result.success = False
229
+ result.errors.append(str(e))
230
+
231
+ return result
232
+
233
+ def uninstall(self, scope: str = "project", dry_run: bool = False) -> UninstallResult:
234
+ result = UninstallResult(success=True, target=self.target, scope=scope)
235
+ target_path = self._get_scope_path(scope)
236
+
237
+ try:
238
+ # Remove .agents/skills/ managed content
239
+ agents_skills_dir = target_path / ".agents" / "skills"
240
+ if agents_skills_dir.exists():
241
+ for item in agents_skills_dir.iterdir():
242
+ if item.is_dir():
243
+ skill_file = item / "SKILL.md"
244
+ if skill_file.exists():
245
+ content = skill_file.read_text(encoding="utf-8")
246
+ begin, end = self._get_markers(skill_file)
247
+ if begin in content:
248
+ if not dry_run:
249
+ shutil.rmtree(item)
250
+ result.files_removed.append(str(item.relative_to(self.project_root)))
251
+
252
+ # Remove .cline/skills/ managed content
253
+ cline_skills_dir = target_path / ".cline" / "skills"
254
+ if cline_skills_dir.exists():
255
+ for item in cline_skills_dir.iterdir():
256
+ if item.is_dir():
257
+ skill_file = item / "SKILL.md"
258
+ if skill_file.exists():
259
+ content = skill_file.read_text(encoding="utf-8")
260
+ begin, end = self._get_markers(skill_file)
261
+ if begin in content:
262
+ if not dry_run:
263
+ shutil.rmtree(item)
264
+ result.files_removed.append(str(item.relative_to(self.project_root)))
265
+
266
+ # Remove rules
267
+ rule_path = target_path / ".clinerules" / "python-skills.md"
268
+ if rule_path.exists() and self._has_ownership_marker(rule_path):
269
+ if not dry_run:
270
+ rule_path.unlink()
271
+ result.files_removed.append(str(rule_path.relative_to(self.project_root)))
272
+
273
+ if not dry_run:
274
+ self._update_lock_state(scope)
275
+
276
+ except Exception as e:
277
+ result.success = False
278
+ result.errors.append(str(e))
279
+
280
+ return result
281
+
282
+ def _has_ownership_marker(self, path: Path) -> bool:
283
+ if not path.exists():
284
+ return False
285
+ content = path.read_text(encoding="utf-8")
286
+ begin, end = self._get_markers(path)
287
+ return begin in content and end in content
288
+
289
+ def status(self, scope: str = "project") -> StatusResult:
290
+ target_path = self._get_scope_path(scope)
291
+ installed = False
292
+ files = []
293
+
294
+ # Check .agents/skills/
295
+ agents_skills_dir = target_path / ".agents" / "skills"
296
+ if agents_skills_dir.exists():
297
+ for item in agents_skills_dir.iterdir():
298
+ if item.is_dir():
299
+ skill_file = item / "SKILL.md"
300
+ if skill_file.exists():
301
+ content = skill_file.read_text(encoding="utf-8")
302
+ begin, end = self._get_markers(skill_file)
303
+ if begin in content:
304
+ installed = True
305
+ files.append(str(skill_file.relative_to(self.project_root)))
306
+
307
+ # Check .cline/skills/
308
+ cline_skills_dir = target_path / ".cline" / "skills"
309
+ if cline_skills_dir.exists():
310
+ for item in cline_skills_dir.iterdir():
311
+ if item.is_dir():
312
+ skill_file = item / "SKILL.md"
313
+ if skill_file.exists():
314
+ content = skill_file.read_text(encoding="utf-8")
315
+ begin, end = self._get_markers(skill_file)
316
+ if begin in content:
317
+ installed = True
318
+ files.append(str(skill_file.relative_to(self.project_root)))
319
+
320
+ # Check .clinerules/
321
+ rule_path = target_path / ".clinerules" / "python-skills.md"
322
+ if rule_path.exists() and self._has_ownership_marker(rule_path):
323
+ installed = True
324
+ files.append(str(rule_path.relative_to(self.project_root)))
325
+
326
+ return StatusResult(
327
+ target=self.target, scope=scope, installed=installed,
328
+ files=files, version="2.0.0"
329
+ )
330
+
331
+ def _update_lock_state(self, scope: str) -> None:
332
+ self.lock_manager.update_canonical_hash(self.skills_registry.skills_root)
@@ -0,0 +1,24 @@
1
+ """Codex CLI adapter for python-skills."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .agent_skills import AgentSkillsAdapter
6
+
7
+
8
+ class CodexAdapter(AgentSkillsAdapter):
9
+ """Adapter for OpenAI Codex CLI.
10
+
11
+ Codex CLI discovers skills from:
12
+ - .agents/skills/<name>/SKILL.md (cross-agent standard)
13
+
14
+ Codex is the primary architect of AGENTS.md.
15
+ """
16
+
17
+ target_name = "codex"
18
+ display_name = "Codex CLI"
19
+ agent_skills_dir = ".agents/skills"
20
+ detection_app_command = "codex"
21
+ detection_project_markers = ["AGENTS.md"]
22
+
23
+ def _get_global_path(self) -> Path:
24
+ return Path.home() / ".agents" / "skills"
@@ -0,0 +1,198 @@
1
+ """Continue adapter for python-skills.
2
+
3
+ Continue uses a native rules system (.continue/rules/) and reads
4
+ AGENTS.md/AGENT.md/CLAUDE.md as fallback instructions.
5
+ It does NOT have a native skills system.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ from ..adapters.base import (
11
+ AgentAdapter,
12
+ DetectionResult,
13
+ InstallResult,
14
+ StatusResult,
15
+ SyncResult,
16
+ UninstallResult,
17
+ )
18
+ from ..config import Target
19
+ from ..skills.registry import SkillRegistry
20
+ from ..state import LockManager
21
+
22
+
23
+ class ContinueAdapter(AgentAdapter):
24
+ """Adapter for Continue.
25
+
26
+ Continue uses rules (.continue/rules/*.md) not skills.
27
+ We generate a consolidated Python rules file.
28
+ """
29
+
30
+ name = "Continue"
31
+ target = Target.CONTINUE.value
32
+
33
+ def __init__(self, project_root: Path, skills_registry: SkillRegistry,
34
+ lock_manager: LockManager, config):
35
+ super().__init__(project_root, skills_registry, lock_manager, config)
36
+
37
+ def _get_global_path(self) -> Path:
38
+ return Path.home() / ".continue" / "rules"
39
+
40
+ def detect(self) -> DetectionResult:
41
+ import shutil
42
+ app_detected = shutil.which("continue") is not None
43
+ project_config = (self.project_root / ".continue").exists()
44
+ global_config = self._get_global_path().exists()
45
+
46
+ return DetectionResult(
47
+ application_detected=app_detected,
48
+ project_config_detected=project_config,
49
+ adapter_available=True,
50
+ details=f"App: {'yes' if app_detected else 'no'}, "
51
+ f"Project: {'yes' if project_config else 'no'}, "
52
+ f"Global: {'yes' if global_config else 'no'}",
53
+ )
54
+
55
+ def install(self, scope: str = "project", dry_run: bool = False) -> InstallResult:
56
+ result = InstallResult(success=True, target=self.target, scope=scope)
57
+ target_path = self._get_scope_path(scope)
58
+
59
+ try:
60
+ rules_dir = target_path / ".continue" / "rules"
61
+ rules_dir.mkdir(parents=True, exist_ok=True)
62
+
63
+ rule_path = rules_dir / "python-skills.md"
64
+ content = self._generate_rule()
65
+
66
+ if dry_run:
67
+ result.files_created.append(str(rule_path.relative_to(self.project_root)))
68
+ else:
69
+ self._safe_write_file(rule_path, content, dry_run)
70
+ result.files_created.append(str(rule_path.relative_to(self.project_root)))
71
+ self._record_file(self.target, scope, rule_path)
72
+
73
+ if not dry_run:
74
+ self._update_lock_state(scope)
75
+
76
+ except Exception as e:
77
+ result.success = False
78
+ result.errors.append(str(e))
79
+
80
+ return result
81
+
82
+ def _generate_rule(self) -> str:
83
+ categories = {}
84
+ if self.skills_registry:
85
+ for skill in self.skills_registry.get_all_skills():
86
+ if skill.category not in categories:
87
+ categories[skill.category] = []
88
+ categories[skill.category].append(skill.name)
89
+
90
+ parts = [
91
+ "---",
92
+ "alwaysApply: true",
93
+ "---",
94
+ "",
95
+ "<!-- BEGIN PYTHON-SKILLS MANAGED -->",
96
+ "",
97
+ "# Python Skills Integration",
98
+ "",
99
+ "This project uses python-skills for Python engineering standards.",
100
+ "",
101
+ ]
102
+
103
+ for category, skills in sorted(categories.items()):
104
+ parts.append(f"### {category.title()}")
105
+ parts.append("")
106
+ for skill in sorted(skills):
107
+ parts.append(f"- `{skill}`")
108
+ parts.append("")
109
+
110
+ parts.extend([
111
+ "## Key Patterns",
112
+ "",
113
+ "- **Type hints**: Use built-in generics, `str | None`, `Protocol`",
114
+ "- **Async**: `asyncio.gather`, `Semaphore`, `TaskGroup` (3.11+)",
115
+ "- **HTTP**: `httpx` with retries, timeouts, connection pooling",
116
+ "- **Security**: Parameterized queries, `pathlib` for paths, `yaml.safe_load`",
117
+ "- **Testing**: `pytest` with parametrized tests, edge cases",
118
+ "",
119
+ "<!-- END PYTHON-SKILLS MANAGED -->",
120
+ ])
121
+
122
+ return "\n".join(parts)
123
+
124
+ def sync(self, scope: str = "project", dry_run: bool = False) -> SyncResult:
125
+ result = SyncResult(success=True, target=self.target, scope=scope)
126
+ target_path = self._get_scope_path(scope)
127
+
128
+ try:
129
+ rule_path = target_path / ".continue" / "rules" / "python-skills.md"
130
+ if not rule_path.exists():
131
+ result.success = False
132
+ result.errors.append("python-skills.md not found")
133
+ return result
134
+
135
+ current = rule_path.read_text(encoding="utf-8")
136
+ new_content = self._generate_rule()
137
+
138
+ if current.strip() != new_content.strip():
139
+ if not dry_run:
140
+ self._safe_write_file(rule_path, new_content, dry_run)
141
+ result.modified.append(str(rule_path.relative_to(self.project_root)))
142
+ else:
143
+ result.skipped.append(str(rule_path.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
+
158
+ try:
159
+ rule_path = target_path / ".continue" / "rules" / "python-skills.md"
160
+ if rule_path.exists():
161
+ content = rule_path.read_text(encoding="utf-8")
162
+ begin, end = self._get_markers(rule_path)
163
+ if begin in content:
164
+ if not dry_run:
165
+ rule_path.unlink()
166
+ result.files_removed.append(str(rule_path.relative_to(self.project_root)))
167
+
168
+ if not dry_run:
169
+ self._update_lock_state(scope)
170
+
171
+ except Exception as e:
172
+ result.success = False
173
+ result.errors.append(str(e))
174
+
175
+ return result
176
+
177
+ def status(self, scope: str = "project") -> StatusResult:
178
+ target_path = self._get_scope_path(scope)
179
+ rule_path = target_path / ".continue" / "rules" / "python-skills.md"
180
+
181
+ installed = False
182
+ files = []
183
+ if rule_path.exists():
184
+ content = rule_path.read_text(encoding="utf-8")
185
+ begin, end = self._get_markers(rule_path)
186
+ if begin in content:
187
+ installed = True
188
+ files.append(str(rule_path.relative_to(self.project_root)))
189
+
190
+ return StatusResult(
191
+ target=self.target, scope=scope, installed=installed,
192
+ files=files, version="2.0.0",
193
+ )
194
+
195
+ def _update_lock_state(self, scope: str) -> None:
196
+ skills_root = self.skills_registry.skills_root
197
+ if skills_root.exists():
198
+ self.lock_manager.update_canonical_hash(skills_root)