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.
Files changed (53) hide show
  1. python_checkup/__init__.py +9 -0
  2. python_checkup/__main__.py +3 -0
  3. python_checkup/analysis_request.py +35 -0
  4. python_checkup/analyzer_catalog.py +100 -0
  5. python_checkup/analyzers/__init__.py +54 -0
  6. python_checkup/analyzers/bandit.py +158 -0
  7. python_checkup/analyzers/basedpyright.py +103 -0
  8. python_checkup/analyzers/cached.py +106 -0
  9. python_checkup/analyzers/dependency_vulns.py +298 -0
  10. python_checkup/analyzers/deptry.py +142 -0
  11. python_checkup/analyzers/detect_secrets.py +101 -0
  12. python_checkup/analyzers/mypy.py +217 -0
  13. python_checkup/analyzers/radon.py +150 -0
  14. python_checkup/analyzers/registry.py +69 -0
  15. python_checkup/analyzers/ruff.py +256 -0
  16. python_checkup/analyzers/typos.py +80 -0
  17. python_checkup/analyzers/vulture.py +151 -0
  18. python_checkup/cache.py +244 -0
  19. python_checkup/cli.py +763 -0
  20. python_checkup/config.py +87 -0
  21. python_checkup/dedup.py +119 -0
  22. python_checkup/dependencies/discovery.py +192 -0
  23. python_checkup/detection.py +298 -0
  24. python_checkup/diff.py +130 -0
  25. python_checkup/discovery.py +180 -0
  26. python_checkup/formatters/__init__.py +0 -0
  27. python_checkup/formatters/badge.py +38 -0
  28. python_checkup/formatters/json_fmt.py +22 -0
  29. python_checkup/formatters/terminal.py +396 -0
  30. python_checkup/mcp/__init__.py +3 -0
  31. python_checkup/mcp/installer.py +119 -0
  32. python_checkup/mcp/server.py +411 -0
  33. python_checkup/models.py +114 -0
  34. python_checkup/plan.py +109 -0
  35. python_checkup/progress.py +95 -0
  36. python_checkup/runner.py +438 -0
  37. python_checkup/scoring/__init__.py +0 -0
  38. python_checkup/scoring/engine.py +397 -0
  39. python_checkup/skills/SKILL.md +416 -0
  40. python_checkup/skills/__init__.py +0 -0
  41. python_checkup/skills/agents.py +98 -0
  42. python_checkup/skills/installer.py +248 -0
  43. python_checkup/skills/rule_db.py +806 -0
  44. python_checkup/web/__init__.py +0 -0
  45. python_checkup/web/server.py +285 -0
  46. python_checkup/web/static/__init__.py +0 -0
  47. python_checkup/web/static/index.html +959 -0
  48. python_checkup/web/template.py +26 -0
  49. python_checkup-0.0.1.dist-info/METADATA +250 -0
  50. python_checkup-0.0.1.dist-info/RECORD +53 -0
  51. python_checkup-0.0.1.dist-info/WHEEL +4 -0
  52. python_checkup-0.0.1.dist-info/entry_points.txt +14 -0
  53. 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