python-checkup 0.0.1__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_checkup/__init__.py +9 -0
- python_checkup/__main__.py +3 -0
- python_checkup/analysis_request.py +35 -0
- python_checkup/analyzer_catalog.py +100 -0
- python_checkup/analyzers/__init__.py +54 -0
- python_checkup/analyzers/bandit.py +158 -0
- python_checkup/analyzers/basedpyright.py +103 -0
- python_checkup/analyzers/cached.py +106 -0
- python_checkup/analyzers/dependency_vulns.py +298 -0
- python_checkup/analyzers/deptry.py +142 -0
- python_checkup/analyzers/detect_secrets.py +101 -0
- python_checkup/analyzers/mypy.py +217 -0
- python_checkup/analyzers/radon.py +150 -0
- python_checkup/analyzers/registry.py +69 -0
- python_checkup/analyzers/ruff.py +256 -0
- python_checkup/analyzers/typos.py +80 -0
- python_checkup/analyzers/vulture.py +151 -0
- python_checkup/cache.py +244 -0
- python_checkup/cli.py +763 -0
- python_checkup/config.py +87 -0
- python_checkup/dedup.py +119 -0
- python_checkup/dependencies/discovery.py +192 -0
- python_checkup/detection.py +298 -0
- python_checkup/diff.py +130 -0
- python_checkup/discovery.py +180 -0
- python_checkup/formatters/__init__.py +0 -0
- python_checkup/formatters/badge.py +38 -0
- python_checkup/formatters/json_fmt.py +22 -0
- python_checkup/formatters/terminal.py +396 -0
- python_checkup/mcp/__init__.py +3 -0
- python_checkup/mcp/installer.py +119 -0
- python_checkup/mcp/server.py +411 -0
- python_checkup/models.py +114 -0
- python_checkup/plan.py +109 -0
- python_checkup/progress.py +95 -0
- python_checkup/runner.py +438 -0
- python_checkup/scoring/__init__.py +0 -0
- python_checkup/scoring/engine.py +397 -0
- python_checkup/skills/SKILL.md +416 -0
- python_checkup/skills/__init__.py +0 -0
- python_checkup/skills/agents.py +98 -0
- python_checkup/skills/installer.py +248 -0
- python_checkup/skills/rule_db.py +806 -0
- python_checkup/web/__init__.py +0 -0
- python_checkup/web/server.py +285 -0
- python_checkup/web/static/__init__.py +0 -0
- python_checkup/web/static/index.html +959 -0
- python_checkup/web/template.py +26 -0
- python_checkup-0.0.1.dist-info/METADATA +250 -0
- python_checkup-0.0.1.dist-info/RECORD +53 -0
- python_checkup-0.0.1.dist-info/WHEEL +4 -0
- python_checkup-0.0.1.dist-info/entry_points.txt +14 -0
- python_checkup-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from python_checkup.skills.agents import (
|
|
10
|
+
AgentTarget,
|
|
11
|
+
detect_installed_agents,
|
|
12
|
+
get_agent_targets,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("python_checkup")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
# Marker used to identify python-checkup content in append-mode files
|
|
19
|
+
APPEND_MARKER_START = "<!-- python-checkup:start -->"
|
|
20
|
+
APPEND_MARKER_END = "<!-- python-checkup:end -->"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def install_skill(
|
|
24
|
+
agents: list[str] | None = None,
|
|
25
|
+
project_level: bool = False,
|
|
26
|
+
force: bool = False,
|
|
27
|
+
) -> list[Path]:
|
|
28
|
+
"""Install SKILL.md and AGENTS.md to detected agent directories.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
agents: Specific agent names to install for. If None, auto-detect.
|
|
32
|
+
project_level: Also install to .agents/python-checkup/ in current dir.
|
|
33
|
+
force: Overwrite existing files without prompting.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of paths that were written to.
|
|
37
|
+
"""
|
|
38
|
+
skill_content = _load_skill_content()
|
|
39
|
+
agents_content = _generate_agents_content(skill_content)
|
|
40
|
+
|
|
41
|
+
if agents:
|
|
42
|
+
all_targets = get_agent_targets()
|
|
43
|
+
targets = [t for t in all_targets if t.name in agents]
|
|
44
|
+
if not targets:
|
|
45
|
+
console.print(
|
|
46
|
+
f"[red]No matching agents found for: {', '.join(agents)}[/red]"
|
|
47
|
+
)
|
|
48
|
+
console.print(
|
|
49
|
+
"Available agents: "
|
|
50
|
+
+ ", ".join(t.name for t in all_targets if t.detection_dirs)
|
|
51
|
+
)
|
|
52
|
+
return []
|
|
53
|
+
else:
|
|
54
|
+
targets = detect_installed_agents()
|
|
55
|
+
|
|
56
|
+
if project_level:
|
|
57
|
+
all_targets = get_agent_targets()
|
|
58
|
+
project_target = next(t for t in all_targets if t.name == "project-level")
|
|
59
|
+
targets.append(project_target)
|
|
60
|
+
|
|
61
|
+
# Default to Claude Code if nothing detected
|
|
62
|
+
if not targets:
|
|
63
|
+
console.print(
|
|
64
|
+
"[yellow]No AI coding agents detected. "
|
|
65
|
+
"Defaulting to Claude Code (~/.claude/).[/yellow]"
|
|
66
|
+
)
|
|
67
|
+
all_targets = get_agent_targets()
|
|
68
|
+
claude_target = next(t for t in all_targets if t.name == "claude-code")
|
|
69
|
+
targets = [claude_target]
|
|
70
|
+
|
|
71
|
+
written_paths: list[Path] = []
|
|
72
|
+
for target in targets:
|
|
73
|
+
paths = _install_to_target(target, skill_content, agents_content, force)
|
|
74
|
+
written_paths.extend(paths)
|
|
75
|
+
|
|
76
|
+
# Summary
|
|
77
|
+
if written_paths:
|
|
78
|
+
console.print()
|
|
79
|
+
table = Table(title="Installed skill files", show_header=True)
|
|
80
|
+
table.add_column("Agent", style="cyan")
|
|
81
|
+
table.add_column("Path", style="dim")
|
|
82
|
+
for target in targets:
|
|
83
|
+
for path in written_paths:
|
|
84
|
+
if str(target.skill_dir) in str(path) or (
|
|
85
|
+
target.global_rules_file
|
|
86
|
+
and str(target.global_rules_file) in str(path)
|
|
87
|
+
):
|
|
88
|
+
table.add_row(target.display_name, str(path))
|
|
89
|
+
console.print(table)
|
|
90
|
+
console.print(
|
|
91
|
+
f"\n[green]Installed python-checkup skill to {len(targets)} agent(s).[/green]"
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
console.print("[yellow]No files were written.[/yellow]")
|
|
95
|
+
|
|
96
|
+
return written_paths
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _install_to_target(
|
|
100
|
+
target: AgentTarget,
|
|
101
|
+
skill_content: str,
|
|
102
|
+
agents_content: str,
|
|
103
|
+
force: bool,
|
|
104
|
+
) -> list[Path]:
|
|
105
|
+
"""Write skill files to a single agent target."""
|
|
106
|
+
paths: list[Path] = []
|
|
107
|
+
|
|
108
|
+
if target.is_append and target.global_rules_file:
|
|
109
|
+
# Windsurf: append to global_rules.md
|
|
110
|
+
path = target.global_rules_file
|
|
111
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
|
|
113
|
+
if path.exists():
|
|
114
|
+
existing = path.read_text()
|
|
115
|
+
# Remove old python-checkup content if present
|
|
116
|
+
if APPEND_MARKER_START in existing:
|
|
117
|
+
before = existing[: existing.index(APPEND_MARKER_START)]
|
|
118
|
+
after_marker = existing.find(APPEND_MARKER_END)
|
|
119
|
+
if after_marker != -1:
|
|
120
|
+
after = existing[after_marker + len(APPEND_MARKER_END) :]
|
|
121
|
+
else:
|
|
122
|
+
after = ""
|
|
123
|
+
existing = before + after
|
|
124
|
+
|
|
125
|
+
# Append new content with markers
|
|
126
|
+
new_content = (
|
|
127
|
+
existing.rstrip()
|
|
128
|
+
+ "\n\n"
|
|
129
|
+
+ APPEND_MARKER_START
|
|
130
|
+
+ "\n"
|
|
131
|
+
+ agents_content
|
|
132
|
+
+ "\n"
|
|
133
|
+
+ APPEND_MARKER_END
|
|
134
|
+
+ "\n"
|
|
135
|
+
)
|
|
136
|
+
path.write_text(new_content)
|
|
137
|
+
else:
|
|
138
|
+
content = (
|
|
139
|
+
APPEND_MARKER_START
|
|
140
|
+
+ "\n"
|
|
141
|
+
+ agents_content
|
|
142
|
+
+ "\n"
|
|
143
|
+
+ APPEND_MARKER_END
|
|
144
|
+
+ "\n"
|
|
145
|
+
)
|
|
146
|
+
path.write_text(content)
|
|
147
|
+
|
|
148
|
+
paths.append(path)
|
|
149
|
+
console.print(f" [green]\u2713[/green] Appended to {path}")
|
|
150
|
+
|
|
151
|
+
else:
|
|
152
|
+
# Standard: write SKILL.md and AGENTS.md to skill directory
|
|
153
|
+
skill_dir = target.skill_dir
|
|
154
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
|
|
156
|
+
skill_path = skill_dir / "SKILL.md"
|
|
157
|
+
agents_path = skill_dir / "AGENTS.md"
|
|
158
|
+
|
|
159
|
+
if skill_path.exists() and not force:
|
|
160
|
+
console.print(
|
|
161
|
+
f" [yellow]\u26a0[/yellow] {skill_path} exists "
|
|
162
|
+
"(use --force to overwrite)"
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
skill_path.write_text(skill_content)
|
|
166
|
+
paths.append(skill_path)
|
|
167
|
+
console.print(f" [green]\u2713[/green] Wrote {skill_path}")
|
|
168
|
+
|
|
169
|
+
if agents_path.exists() and not force:
|
|
170
|
+
console.print(
|
|
171
|
+
f" [yellow]\u26a0[/yellow] {agents_path} exists "
|
|
172
|
+
"(use --force to overwrite)"
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
agents_path.write_text(agents_content)
|
|
176
|
+
paths.append(agents_path)
|
|
177
|
+
console.print(f" [green]\u2713[/green] Wrote {agents_path}")
|
|
178
|
+
|
|
179
|
+
return paths
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _load_skill_content() -> str:
|
|
183
|
+
"""Load the SKILL.md template and inject current rule data."""
|
|
184
|
+
# The SKILL.md template is bundled as package data
|
|
185
|
+
skill_path = Path(__file__).parent / "SKILL.md"
|
|
186
|
+
if skill_path.exists():
|
|
187
|
+
return skill_path.read_text()
|
|
188
|
+
|
|
189
|
+
# Fallback: generate from rule database
|
|
190
|
+
from python_checkup.skills.rule_db import generate_skill_content
|
|
191
|
+
|
|
192
|
+
return generate_skill_content()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _generate_agents_content(skill_content: str) -> str:
|
|
196
|
+
"""Strip YAML frontmatter from SKILL.md to produce AGENTS.md content."""
|
|
197
|
+
lines = skill_content.split("\n")
|
|
198
|
+
if lines and lines[0].strip() == "---":
|
|
199
|
+
for i, line in enumerate(lines[1:], start=1):
|
|
200
|
+
if line.strip() == "---":
|
|
201
|
+
return "\n".join(lines[i + 1 :]).lstrip("\n")
|
|
202
|
+
return skill_content
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def uninstall_skill(agents: list[str] | None = None) -> list[Path]:
|
|
206
|
+
"""Remove installed skill files.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
agents: Specific agents to uninstall from. If None, remove from all.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of paths that were removed.
|
|
213
|
+
"""
|
|
214
|
+
if agents:
|
|
215
|
+
all_targets = get_agent_targets()
|
|
216
|
+
targets = [t for t in all_targets if t.name in agents]
|
|
217
|
+
else:
|
|
218
|
+
targets = get_agent_targets()
|
|
219
|
+
|
|
220
|
+
removed: list[Path] = []
|
|
221
|
+
for target in targets:
|
|
222
|
+
if target.is_append and target.global_rules_file:
|
|
223
|
+
path = target.global_rules_file
|
|
224
|
+
if path.exists():
|
|
225
|
+
content = path.read_text()
|
|
226
|
+
if APPEND_MARKER_START in content:
|
|
227
|
+
before = content[: content.index(APPEND_MARKER_START)]
|
|
228
|
+
after_marker = content.find(APPEND_MARKER_END)
|
|
229
|
+
if after_marker != -1:
|
|
230
|
+
after = content[after_marker + len(APPEND_MARKER_END) :]
|
|
231
|
+
else:
|
|
232
|
+
after = ""
|
|
233
|
+
path.write_text((before + after).strip() + "\n")
|
|
234
|
+
removed.append(path)
|
|
235
|
+
console.print(f" [green]\u2713[/green] Removed from {path}")
|
|
236
|
+
else:
|
|
237
|
+
skill_dir = target.skill_dir
|
|
238
|
+
for filename in ("SKILL.md", "AGENTS.md"):
|
|
239
|
+
filepath = skill_dir / filename
|
|
240
|
+
if filepath.exists():
|
|
241
|
+
filepath.unlink()
|
|
242
|
+
removed.append(filepath)
|
|
243
|
+
console.print(f" [green]\u2713[/green] Removed {filepath}")
|
|
244
|
+
# Remove directory if empty
|
|
245
|
+
if skill_dir.exists() and not any(skill_dir.iterdir()):
|
|
246
|
+
skill_dir.rmdir()
|
|
247
|
+
|
|
248
|
+
return removed
|