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,27 @@
1
+ """Zed adapter for python-skills."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .agent_skills import AgentSkillsAdapter
6
+
7
+
8
+ class ZedAdapter(AgentSkillsAdapter):
9
+ """Adapter for Zed editor.
10
+
11
+ Zed discovers skills from:
12
+ - .agents/skills/<name>/SKILL.md (Agent Skills spec)
13
+
14
+ Zed also reads AGENTS.md for instructions.
15
+ """
16
+
17
+ target_name = "zed"
18
+ display_name = "Zed"
19
+ agent_skills_dir = ".agents/skills"
20
+ detection_app_command = "zed"
21
+ detection_project_markers = ["AGENTS.md", ".agents"]
22
+
23
+ def _get_global_path(self) -> Path:
24
+ import platform
25
+ if platform.system() == "Windows":
26
+ return Path.home() / "AppData" / "Roaming" / "Zed" / "skills"
27
+ return Path.home() / ".config" / "zed" / "skills"
python_skills/cli.py ADDED
@@ -0,0 +1,326 @@
1
+ """CLI for python-skills."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from python_skills.config import InstallConfig, Scope, Target
12
+ from python_skills.detector import EnvironmentDetector
13
+ from python_skills.installer import SkillInstaller
14
+
15
+ console = Console()
16
+
17
+
18
+ @click.group()
19
+ @click.option("--project-root", "-p", type=click.Path(exists=True, file_okay=False, path_type=Path),
20
+ default=".", help="Project root directory")
21
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
22
+ @click.pass_context
23
+ def main(ctx: click.Context, project_root: Path, dry_run: bool):
24
+ """Python Skills - Python engineering skills for AI coding agents."""
25
+ ctx.ensure_object(dict)
26
+ ctx.obj["project_root"] = project_root
27
+ ctx.obj["dry_run"] = dry_run
28
+
29
+
30
+ @main.command()
31
+ @click.option("--target", "-t", multiple=True,
32
+ type=click.Choice([t.value for t in Target], case_sensitive=False),
33
+ help="Target AI coding agent(s)")
34
+ @click.option("--scope", "-s", type=click.Choice([s.value for s in Scope], case_sensitive=False),
35
+ default="project", help="Installation scope")
36
+ @click.option("--auto/--no-auto", default=False, help="Auto-detect and install all compatible targets")
37
+ @click.pass_context
38
+ def install(ctx: click.Context, target: list[str], scope: str, auto: bool):
39
+ """Install python-skills for AI coding agents."""
40
+ project_root = ctx.obj["project_root"]
41
+ dry_run = ctx.obj["dry_run"]
42
+ scope_enum = Scope(scope)
43
+
44
+ console.print(Panel.fit(
45
+ f"[bold]Python Skills Installer[/bold]\n"
46
+ f"Project: {project_root}\n"
47
+ f"Scope: {scope}\n"
48
+ f"Dry run: {dry_run}",
49
+ title="Installation"
50
+ ))
51
+
52
+ # Parse targets
53
+ targets = [Target(t.lower()) for t in target] if target else None
54
+
55
+ # Create config
56
+ config = InstallConfig(
57
+ targets=targets or [],
58
+ scope=scope_enum,
59
+ dry_run=dry_run
60
+ )
61
+
62
+ # Create installer
63
+ installer = SkillInstaller(project_root, config)
64
+
65
+ # Detect environments
66
+ detection = installer.detect_environments()
67
+
68
+ # Print detection results
69
+ table = Table(title="Environment Detection")
70
+ table.add_column("Target", style="cyan")
71
+ table.add_column("Application", style="green")
72
+ table.add_column("Project Config", style="yellow")
73
+ table.add_column("Details", style="dim")
74
+
75
+ for name, result in detection.targets.items():
76
+ app_status = "Yes" if result.application_detected else "No"
77
+ proj_status = "Yes" if result.project_config_detected else "No"
78
+ table.add_row(name, app_status, proj_status, result.details)
79
+
80
+ console.print(table)
81
+ console.print()
82
+
83
+ # Plan installation
84
+ plan = installer.plan_installation(
85
+ targets=targets,
86
+ scope=scope_enum,
87
+ auto=auto
88
+ )
89
+
90
+ if not plan.targets:
91
+ console.print("[yellow]No targets to install[/yellow]")
92
+ return
93
+
94
+ # Print plan
95
+ plan_table = Table(title="Installation Plan")
96
+ plan_table.add_column("Target", style="cyan")
97
+ plan_table.add_column("Scope", style="green")
98
+ plan_table.add_column("Dry Run", style="yellow")
99
+
100
+ for target, scope_val in plan.targets.items():
101
+ plan_table.add_row(target.value, scope_val.value, "Yes" if plan.dry_run else "No")
102
+
103
+ console.print(plan_table)
104
+ console.print()
105
+
106
+ if dry_run:
107
+ console.print("[yellow]DRY RUN - No changes will be made[/yellow]")
108
+ return
109
+
110
+ # Confirm
111
+ if not click.confirm("Proceed with installation?"):
112
+ console.print("[red]Installation cancelled[/red]")
113
+ return
114
+
115
+ # Execute installation
116
+ console.print("\n[bold]Installing...[/bold]")
117
+ results = installer.execute_install(plan)
118
+
119
+ # Summary
120
+ success_count = sum(1 for r in results.values() if r.success)
121
+ total_count = len(results)
122
+
123
+ console.print(f"\n[bold]Installation complete: {success_count}/{total_count} successful[/bold]")
124
+
125
+ if success_count < total_count:
126
+ sys.exit(1)
127
+
128
+
129
+ @main.command()
130
+ @click.option("--target", "-t", multiple=True,
131
+ type=click.Choice([t.value for t in Target], case_sensitive=False),
132
+ help="Target(s) to sync")
133
+ @click.option("--scope", "-s", type=click.Choice([s.value for s in Scope], case_sensitive=False),
134
+ default="project", help="Sync scope")
135
+ @click.pass_context
136
+ def sync(ctx: click.Context, target: list[str], scope: str):
137
+ """Sync python-skills with current canonical skills."""
138
+ project_root = ctx.obj["project_root"]
139
+ dry_run = ctx.obj["dry_run"]
140
+ scope_enum = Scope(scope)
141
+
142
+ console.print(Panel.fit(
143
+ f"[bold]Python Skills Sync[/bold]\n"
144
+ f"Project: {project_root}\n"
145
+ f"Scope: {scope}\n"
146
+ f"Dry run: {dry_run}",
147
+ title="Sync"
148
+ ))
149
+
150
+ config = InstallConfig(targets=[], scope=scope_enum, dry_run=dry_run)
151
+ installer = SkillInstaller(project_root, config)
152
+
153
+ targets = [Target(t.lower()) for t in target] if target else None
154
+
155
+ if dry_run:
156
+ console.print("[yellow]DRY RUN - No changes will be made[/yellow]")
157
+
158
+ console.print("\n[bold]Syncing...[/bold]")
159
+ results = installer.run_sync(scope=scope_enum, targets=targets, dry_run=dry_run)
160
+
161
+ # Summary
162
+ success_count = sum(1 for r in results.values() if r.success)
163
+ total_count = len(results)
164
+
165
+ console.print(f"\n[bold]Sync complete: {success_count}/{total_count} successful[/bold]")
166
+
167
+ if success_count < total_count:
168
+ sys.exit(1)
169
+
170
+
171
+ @main.command()
172
+ @click.option("--target", "-t", multiple=True,
173
+ type=click.Choice([t.value for t in Target], case_sensitive=False),
174
+ help="Target(s) to uninstall")
175
+ @click.option("--scope", "-s", type=click.Choice([s.value for s in Scope], case_sensitive=False),
176
+ default="project", help="Uninstall scope")
177
+ @click.pass_context
178
+ def uninstall(ctx: click.Context, target: list[str], scope: str):
179
+ """Uninstall python-skills for AI coding agents."""
180
+ project_root = ctx.obj["project_root"]
181
+ dry_run = ctx.obj["dry_run"]
182
+ scope_enum = Scope(scope)
183
+
184
+ console.print(Panel.fit(
185
+ f"[bold]Python Skills Uninstaller[/bold]\n"
186
+ f"Project: {project_root}\n"
187
+ f"Scope: {scope}\n"
188
+ f"Dry run: {dry_run}",
189
+ title="Uninstallation"
190
+ ))
191
+
192
+ targets = [Target(t.lower()) for t in target] if target else None
193
+
194
+ config = InstallConfig(targets=targets or [], scope=scope_enum, dry_run=dry_run)
195
+ installer = SkillInstaller(project_root, config)
196
+
197
+ if dry_run:
198
+ console.print("[yellow]DRY RUN - No changes will be made[/yellow]")
199
+
200
+ console.print("\n[bold]Uninstalling...[/bold]")
201
+ results = installer.run_uninstall(scope=scope_enum, targets=targets, dry_run=dry_run)
202
+
203
+ success_count = sum(1 for r in results.values() if r.success)
204
+ total_count = len(results)
205
+
206
+ console.print(f"\n[bold]Uninstallation complete: {success_count}/{total_count} successful[/bold]")
207
+
208
+ if success_count < total_count:
209
+ sys.exit(1)
210
+
211
+
212
+ @main.command()
213
+ @click.option("--scope", "-s", type=click.Choice([s.value for s in Scope], case_sensitive=False),
214
+ default="project", help="Status scope")
215
+ @click.pass_context
216
+ def status(ctx: click.Context, scope: str):
217
+ """Show installation status."""
218
+ project_root = ctx.obj["project_root"]
219
+ scope_enum = Scope(scope)
220
+
221
+ console.print(Panel.fit(
222
+ f"[bold]Python Skills Status[/bold]\n"
223
+ f"Project: {project_root}\n"
224
+ f"Scope: {scope}",
225
+ title="Status"
226
+ ))
227
+
228
+ config = InstallConfig(targets=[], scope=scope_enum, dry_run=False)
229
+ installer = SkillInstaller(project_root, config)
230
+
231
+ console.print("\n[bold]Installation Status[/bold]")
232
+ results = installer.get_status(scope=scope_enum)
233
+
234
+ # Summary table
235
+ table = Table()
236
+ table.add_column("Target", style="cyan")
237
+ table.add_column("Status", style="green")
238
+ table.add_column("Files", style="yellow")
239
+
240
+ for name, result in results.items():
241
+ status = "Installed" if result.installed else "Not installed"
242
+ table.add_row(name, status, str(len(result.files)))
243
+
244
+ console.print(table)
245
+
246
+
247
+ @main.command()
248
+ @click.option("--scope", "-s", type=click.Choice([s.value for s in Scope], case_sensitive=False),
249
+ default="project", help="Detection scope")
250
+ @click.option("--project-root", "-p", type=click.Path(exists=True, file_okay=False, path_type=Path),
251
+ default=".", help="Project root directory")
252
+ @click.pass_context
253
+ def detect(ctx: click.Context, scope: str, project_root: Path):
254
+ """Detect AI coding agent environments."""
255
+ scope_enum = Scope(scope)
256
+
257
+ console.print(Panel.fit(
258
+ f"[bold]Environment Detection[/bold]\n"
259
+ f"Project: {project_root}\n"
260
+ f"Scope: {scope}",
261
+ title="Detection"
262
+ ))
263
+
264
+ detector = EnvironmentDetector(project_root)
265
+ detection = detector.detect_all()
266
+
267
+ table = Table(title="Environment Detection Results")
268
+ table.add_column("Target", style="cyan")
269
+ table.add_column("Application", style="green")
270
+ table.add_column("Project Config", style="yellow")
271
+ table.add_column("Global Config", style="blue")
272
+ table.add_column("Details", style="dim")
273
+
274
+ for name, result in detection.targets.items():
275
+ app = "Yes" if result.application_detected else "No"
276
+ proj = "Yes" if result.project_config_detected else "No"
277
+ glob = "Yes" if (result.details and "Global: yes" in result.details) else "No"
278
+ table.add_row(name, app, proj, glob, result.details)
279
+
280
+ console.print(table)
281
+
282
+
283
+ @main.command()
284
+ @click.option("--category", "-c", help="Filter by category")
285
+ def list(category: str):
286
+ """List available skills and supported targets."""
287
+ from python_skills.skills.registry import get_registry
288
+
289
+ registry = get_registry()
290
+
291
+ # List targets
292
+ console.print(Panel.fit(
293
+ "[bold]Supported Targets (16)[/bold]\n"
294
+ + "\n".join(f" {t.value}" for t in Target),
295
+ title="Targets"
296
+ ))
297
+
298
+ # List skills
299
+ if category:
300
+ skills = registry.get_skills_by_category(category)
301
+ title = f"Skills in '{category}'"
302
+ else:
303
+ skills = registry.get_all_skills()
304
+ title = f"All Skills ({len(skills)})"
305
+
306
+ table = Table(title=title)
307
+ table.add_column("Name", style="cyan")
308
+ table.add_column("Category", style="green")
309
+ table.add_column("Description", style="dim")
310
+
311
+ for skill in sorted(skills, key=lambda s: s.name):
312
+ desc = skill.description[:60] + "..." if len(skill.description) > 60 else skill.description
313
+ table.add_row(skill.name, skill.category, desc)
314
+
315
+ console.print(table)
316
+
317
+
318
+ @main.command()
319
+ def version():
320
+ """Show version."""
321
+ from python_skills import __version__
322
+ console.print(f"python-skills v{__version__}")
323
+
324
+
325
+ if __name__ == "__main__":
326
+ main()
@@ -0,0 +1,160 @@
1
+ """Configuration management for python-skills."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+
7
+ class Scope(str, Enum):
8
+ """Installation scope."""
9
+ PROJECT = "project"
10
+ GLOBAL = "global"
11
+
12
+
13
+ class Target(str, Enum):
14
+ """Target AI coding agent."""
15
+ CLAUDE = "claude"
16
+ CURSOR = "cursor"
17
+ KIRO = "kiro"
18
+ CLINE = "cline"
19
+ OPENCODE = "opencode"
20
+ WINDSURF = "windsurf"
21
+ VSCODE = "vscode"
22
+ ROO = "roo"
23
+ GEMINI = "gemini"
24
+ CODEX = "codex"
25
+ JETBRAINS = "jetbrains"
26
+ GOOSE = "goose"
27
+ ZED = "zed"
28
+ CONTINUE = "continue"
29
+ AIDER = "aider"
30
+ UNIVERSAL = "universal"
31
+
32
+
33
+ @dataclass
34
+ class AdapterCapabilities:
35
+ """Capabilities of an adapter."""
36
+ supports_project: bool = True
37
+ supports_global: bool = False
38
+ has_native_skills: bool = False
39
+ has_native_rules: bool = False
40
+ has_native_instructions: bool = False
41
+ supports_path_conditions: bool = False
42
+ supports_file_references: bool = False
43
+ supports_manual_activation: bool = False
44
+ supports_auto_activation: bool = True
45
+ supports_cli: bool = False
46
+ supports_ide: bool = False
47
+ safe_uninstall: bool = True
48
+ adapter_class: str = "E" # A=NativeSkills, B=NativeRules, C=NativeInstructions, D=Config, E=Universal
49
+
50
+
51
+ ADAPTER_CAPABILITIES = {
52
+ Target.CLAUDE: AdapterCapabilities(
53
+ supports_project=True, supports_global=True,
54
+ has_native_skills=True, has_native_rules=True,
55
+ has_native_instructions=True, supports_path_conditions=True,
56
+ supports_file_references=True, supports_cli=True,
57
+ adapter_class="A",
58
+ ),
59
+ Target.CURSOR: AdapterCapabilities(
60
+ supports_project=True, supports_global=False,
61
+ has_native_rules=True, supports_path_conditions=True,
62
+ supports_ide=True, adapter_class="B",
63
+ ),
64
+ Target.KIRO: AdapterCapabilities(
65
+ supports_project=True, supports_global=True,
66
+ has_native_skills=True, has_native_rules=True,
67
+ has_native_instructions=True, supports_path_conditions=True,
68
+ supports_ide=True, adapter_class="A",
69
+ ),
70
+ Target.CLINE: AdapterCapabilities(
71
+ supports_project=True, supports_global=True,
72
+ has_native_skills=True, has_native_rules=True,
73
+ has_native_instructions=True, supports_path_conditions=True,
74
+ supports_ide=True, adapter_class="A",
75
+ ),
76
+ Target.OPENCODE: AdapterCapabilities(
77
+ supports_project=True, supports_global=True,
78
+ has_native_skills=True, has_native_instructions=True,
79
+ supports_file_references=True,
80
+ supports_cli=True, supports_ide=True,
81
+ adapter_class="A",
82
+ ),
83
+ Target.WINDSURF: AdapterCapabilities(
84
+ supports_project=True, supports_global=True,
85
+ has_native_skills=True, has_native_rules=True,
86
+ has_native_instructions=True, supports_path_conditions=True,
87
+ supports_ide=True, adapter_class="A",
88
+ ),
89
+ Target.VSCODE: AdapterCapabilities(
90
+ supports_project=True, supports_global=True,
91
+ has_native_skills=True, has_native_rules=True,
92
+ has_native_instructions=True, supports_path_conditions=True,
93
+ supports_file_references=True,
94
+ supports_cli=True, supports_ide=True,
95
+ adapter_class="A",
96
+ ),
97
+ Target.ROO: AdapterCapabilities(
98
+ supports_project=True, supports_global=True,
99
+ has_native_skills=True, has_native_rules=True,
100
+ has_native_instructions=True,
101
+ supports_ide=True, adapter_class="A",
102
+ ),
103
+ Target.GEMINI: AdapterCapabilities(
104
+ supports_project=True, supports_global=True,
105
+ has_native_skills=True, has_native_instructions=True,
106
+ supports_file_references=True,
107
+ supports_cli=True, adapter_class="A",
108
+ ),
109
+ Target.CODEX: AdapterCapabilities(
110
+ supports_project=True, supports_global=True,
111
+ has_native_skills=True, has_native_instructions=True,
112
+ supports_cli=True, adapter_class="A",
113
+ ),
114
+ Target.JETBRAINS: AdapterCapabilities(
115
+ supports_project=True, supports_global=True,
116
+ has_native_skills=True, has_native_instructions=True,
117
+ supports_ide=True, adapter_class="A",
118
+ ),
119
+ Target.GOOSE: AdapterCapabilities(
120
+ supports_project=True, supports_global=True,
121
+ has_native_skills=True, has_native_instructions=True,
122
+ supports_file_references=True,
123
+ supports_cli=True, adapter_class="A",
124
+ ),
125
+ Target.ZED: AdapterCapabilities(
126
+ supports_project=True, supports_global=True,
127
+ has_native_skills=True, has_native_instructions=True,
128
+ supports_ide=True, adapter_class="A",
129
+ ),
130
+ Target.CONTINUE: AdapterCapabilities(
131
+ supports_project=True, supports_global=True,
132
+ has_native_rules=True, has_native_instructions=True,
133
+ supports_path_conditions=True,
134
+ supports_ide=True, adapter_class="B",
135
+ ),
136
+ Target.AIDER: AdapterCapabilities(
137
+ supports_project=True, supports_global=True,
138
+ has_native_instructions=True,
139
+ supports_cli=True, adapter_class="D",
140
+ ),
141
+ Target.UNIVERSAL: AdapterCapabilities(
142
+ supports_project=True, supports_global=True,
143
+ has_native_instructions=True,
144
+ supports_cli=True, supports_ide=True,
145
+ adapter_class="E",
146
+ ),
147
+ }
148
+
149
+
150
+ @dataclass
151
+ class InstallConfig:
152
+ """Configuration for installation."""
153
+ targets: list[Target]
154
+ scope: Scope = Scope.PROJECT
155
+ dry_run: bool = False
156
+
157
+
158
+ def get_adapter_capabilities(target: Target) -> AdapterCapabilities:
159
+ """Get capabilities for a target adapter."""
160
+ return ADAPTER_CAPABILITIES.get(target, AdapterCapabilities())
@@ -0,0 +1,152 @@
1
+ """Environment detection for AI coding agents."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ from python_skills.adapters.base import DetectionResult
9
+ from python_skills.config import Target
10
+
11
+
12
+ @dataclass
13
+ class DetectionSummary:
14
+ """Summary of detection results for all targets."""
15
+ targets: dict[str, DetectionResult] = field(default_factory=dict)
16
+
17
+ def get_available_targets(self) -> list[str]:
18
+ """Get list of targets with detected application."""
19
+ return [name for name, result in self.targets.items() if result.application_detected]
20
+
21
+
22
+ class EnvironmentDetector:
23
+ """Detects AI coding agent environments and configurations."""
24
+
25
+ def __init__(self, project_root: Path):
26
+ self.project_root = Path(project_root).resolve()
27
+
28
+ def detect_all(self) -> DetectionSummary:
29
+ """Detect all supported AI coding agents."""
30
+ summary = DetectionSummary()
31
+
32
+ # Existing targets
33
+ summary.targets[Target.CLAUDE.value] = self._detect_claude()
34
+ summary.targets[Target.CURSOR.value] = self._detect_cursor()
35
+ summary.targets[Target.KIRO.value] = self._detect_kiro()
36
+ summary.targets[Target.CLINE.value] = self._detect_cline()
37
+
38
+ # Phase 4 targets
39
+ summary.targets[Target.OPENCODE.value] = self._detect_app("opencode", [".opencode", "opencode.json", "opencode.jsonc"])
40
+ summary.targets[Target.WINDSURF.value] = self._detect_dir_only([".windsurf", ".devin"])
41
+ summary.targets[Target.VSCODE.value] = self._detect_app("code", [".github", ".vscode"])
42
+ summary.targets[Target.ROO.value] = self._detect_dir_only([".roo", ".roorules"])
43
+ summary.targets[Target.GEMINI.value] = self._detect_app("gemini", [".gemini", "GEMINI.md"])
44
+ summary.targets[Target.CODEX.value] = self._detect_app("codex", ["AGENTS.md"])
45
+ summary.targets[Target.JETBRAINS.value] = self._detect_dir_only([".junie"])
46
+ summary.targets[Target.GOOSE.value] = self._detect_app("goose", [".goosehints", "AGENTS.md"])
47
+ summary.targets[Target.ZED.value] = self._detect_app("zed", ["AGENTS.md", ".agents"])
48
+ summary.targets[Target.CONTINUE.value] = self._detect_dir_only([".continue"])
49
+ summary.targets[Target.AIDER.value] = self._detect_app("aider", [".aider.conf.yml"])
50
+
51
+ # Universal (always available)
52
+ summary.targets[Target.UNIVERSAL.value] = DetectionResult(
53
+ application_detected=True,
54
+ project_config_detected=(self.project_root / "AGENTS.md").exists(),
55
+ adapter_available=True,
56
+ details="Universal adapter always available",
57
+ )
58
+
59
+ return summary
60
+
61
+ def _detect_app(self, command: str, project_markers: list[str]) -> DetectionResult:
62
+ """Detect application by command and project markers."""
63
+ app_detected = shutil.which(command) is not None
64
+ project_config = any((self.project_root / m).exists() for m in project_markers)
65
+
66
+ return DetectionResult(
67
+ application_detected=app_detected,
68
+ project_config_detected=project_config,
69
+ adapter_available=True,
70
+ details=f"App: {'yes' if app_detected else 'no'}, Project: {'yes' if project_config else 'no'}",
71
+ )
72
+
73
+ def _detect_dir_only(self, project_markers: list[str]) -> DetectionResult:
74
+ """Detect by project markers only (no CLI command)."""
75
+ project_config = any((self.project_root / m).exists() for m in project_markers)
76
+
77
+ return DetectionResult(
78
+ application_detected=False,
79
+ project_config_detected=project_config,
80
+ adapter_available=True,
81
+ details=f"App: IDE extension, Project: {'yes' if project_config else 'no'}",
82
+ )
83
+
84
+ def _detect_claude(self) -> DetectionResult:
85
+ """Detect Claude Code."""
86
+ app_detected = shutil.which("claude") is not None
87
+ project_config = (self.project_root / ".claude").exists()
88
+ global_config = (Path.home() / ".claude").exists()
89
+
90
+ return DetectionResult(
91
+ application_detected=app_detected,
92
+ project_config_detected=project_config,
93
+ adapter_available=True,
94
+ details=f"App: {'yes' if app_detected else 'no'}, Project: {'yes' if project_config else 'no'}, Global: {'yes' if global_config else 'no'}"
95
+ )
96
+
97
+ def _detect_cursor(self) -> DetectionResult:
98
+ """Detect Cursor."""
99
+ app_detected = shutil.which("cursor") is not None
100
+ project_config = (self.project_root / ".cursor").exists() or (self.project_root / ".cursorrules").exists()
101
+
102
+ return DetectionResult(
103
+ application_detected=app_detected,
104
+ project_config_detected=project_config,
105
+ adapter_available=True,
106
+ details=f"App: {'yes' if app_detected else 'no'}, Project: {'yes' if project_config else 'no'}"
107
+ )
108
+
109
+ def _detect_kiro(self) -> DetectionResult:
110
+ """Detect Kiro."""
111
+ app_detected = shutil.which("kiro") is not None
112
+ project_config = (self.project_root / ".kiro").exists()
113
+ global_config = (Path.home() / ".kiro").exists()
114
+
115
+ return DetectionResult(
116
+ application_detected=app_detected,
117
+ project_config_detected=project_config,
118
+ adapter_available=True,
119
+ details=f"App: {'yes' if app_detected else 'no'}, Project: {'yes' if project_config else 'no'}, Global: {'yes' if global_config else 'no'}"
120
+ )
121
+
122
+ def _detect_cline(self) -> DetectionResult:
123
+ """Detect Cline."""
124
+ app_detected = False
125
+ try:
126
+ result = subprocess.run(
127
+ ["code", "--list-extensions"],
128
+ capture_output=True, text=True, timeout=5
129
+ )
130
+ if "cline.cline" in result.stdout.lower():
131
+ app_detected = True
132
+ except Exception:
133
+ pass
134
+
135
+ project_config = (
136
+ (self.project_root / ".clinerules").exists() or
137
+ (self.project_root / ".cline").exists() or
138
+ (self.project_root / ".agents" / "skills").exists()
139
+ )
140
+
141
+ return DetectionResult(
142
+ application_detected=app_detected,
143
+ project_config_detected=project_config,
144
+ adapter_available=True,
145
+ details=f"App: {'yes' if app_detected else 'no'}, Project: {'yes' if project_config else 'no'}"
146
+ )
147
+
148
+
149
+ def detect_environment(project_root: Path) -> 'DetectionSummary':
150
+ """Convenience function to detect all environments."""
151
+ detector = EnvironmentDetector(project_root)
152
+ return detector.detect_all()