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,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()
|
python_skills/config.py
ADDED
|
@@ -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()
|