repocheck-cli 0.1.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.
repocheck/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """repocheck — scan a GitHub repo and get a health report card."""
2
+
3
+ __version__ = "0.1.0"
repocheck/checks.py ADDED
@@ -0,0 +1,293 @@
1
+ """Individual health checks. Each returns a CheckResult with a 0-100 score."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from typing import Callable
8
+
9
+ from .github_client import GitHubClient, RepoRef
10
+
11
+ DEPENDENCY_FILES = (
12
+ "requirements.txt",
13
+ "package.json",
14
+ "Pipfile",
15
+ "pyproject.toml",
16
+ "go.mod",
17
+ "Cargo.toml",
18
+ )
19
+
20
+ TEST_DIR_HINTS = ("test", "tests", "__tests__", "spec")
21
+ TEST_CONFIG_HINTS = (
22
+ "pytest.ini",
23
+ "setup.cfg",
24
+ "tox.ini",
25
+ "jest.config.js",
26
+ "jest.config.ts",
27
+ "vitest.config.ts",
28
+ "phpunit.xml",
29
+ )
30
+
31
+
32
+ @dataclass
33
+ class CheckResult:
34
+ name: str
35
+ score: int # 0-100
36
+ grade: str
37
+ details: list[str] = field(default_factory=list)
38
+ weight: float = 1.0
39
+
40
+ @staticmethod
41
+ def grade_for(score: int) -> str:
42
+ if score >= 90:
43
+ return "A"
44
+ if score >= 80:
45
+ return "B"
46
+ if score >= 70:
47
+ return "C"
48
+ if score >= 60:
49
+ return "D"
50
+ return "F"
51
+
52
+
53
+ def _result(name: str, score: int, details: list[str], weight: float = 1.0) -> CheckResult:
54
+ score = max(0, min(100, score))
55
+ return CheckResult(name=name, score=score, grade=CheckResult.grade_for(score), details=details, weight=weight)
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Individual checks
60
+ # ---------------------------------------------------------------------------
61
+
62
+ def check_readme(client: GitHubClient, ref: RepoRef) -> CheckResult:
63
+ files = client.root_files(ref)
64
+ readme_name = next((f for f in files if f.lower().startswith("readme")), None)
65
+
66
+ if not readme_name:
67
+ return _result("README", 0, ["No README file found at repo root."])
68
+
69
+ text = client.file_text(ref, readme_name) or ""
70
+ details = [f"Found {readme_name} ({len(text)} chars)."]
71
+ score = 30 # baseline for existing
72
+
73
+ length = len(text)
74
+ if length > 300:
75
+ score += 15
76
+ details.append("Has substantive content (>300 chars).")
77
+ else:
78
+ details.append("Very short — consider expanding (currently <300 chars).")
79
+
80
+ lower = text.lower()
81
+
82
+ has_install = bool(re.search(r"\b(install|pip install|npm install|getting started|setup)\b", lower))
83
+ if has_install:
84
+ score += 20
85
+ details.append("Includes install/setup instructions.")
86
+ else:
87
+ details.append("No clear install/setup section found.")
88
+
89
+ has_usage = bool(re.search(r"\b(usage|example|quick ?start)\b", lower))
90
+ if has_usage:
91
+ score += 20
92
+ details.append("Includes usage/example section.")
93
+ else:
94
+ details.append("No usage or example section found.")
95
+
96
+ has_badges = "![" in text and ("shields.io" in text or "badge" in lower)
97
+ if has_badges:
98
+ score += 10
99
+ details.append("Has status badges (CI, version, etc).")
100
+
101
+ has_code_block = "```" in text
102
+ if has_code_block:
103
+ score += 5
104
+ details.append("Contains code blocks.")
105
+
106
+ return _result("README Quality", score, details)
107
+
108
+
109
+ def check_ci(client: GitHubClient, ref: RepoRef) -> CheckResult:
110
+ workflows = client.workflows(ref)
111
+ if workflows:
112
+ names = [w["name"] for w in workflows if w.get("type") == "file"]
113
+ return _result(
114
+ "CI/CD",
115
+ 90 if len(names) >= 1 else 70,
116
+ [f"GitHub Actions configured ({len(names)} workflow file(s)): {', '.join(names) or 'unnamed'}."],
117
+ )
118
+
119
+ root_files = client.root_files(ref)
120
+ legacy = [f for f in root_files if f in (".travis.yml", ".circleci", "azure-pipelines.yml", ".gitlab-ci.yml")]
121
+ if legacy:
122
+ return _result("CI/CD", 60, [f"Legacy CI config found: {', '.join(legacy)}. Consider migrating to GitHub Actions."])
123
+
124
+ return _result("CI/CD", 0, ["No CI/CD configuration found (no .github/workflows, no legacy CI config)."])
125
+
126
+
127
+ def check_tests(client: GitHubClient, ref: RepoRef) -> CheckResult:
128
+ dirs = [d.lower() for d in client.root_dirs(ref)]
129
+ files = [f.lower() for f in client.root_files(ref)]
130
+
131
+ has_test_dir = any(hint in dirs for hint in TEST_DIR_HINTS)
132
+ has_test_config = any(hint in files for hint in TEST_CONFIG_HINTS)
133
+
134
+ details = []
135
+ score = 0
136
+
137
+ if has_test_dir:
138
+ score += 60
139
+ matched = [d for d in dirs if d in TEST_DIR_HINTS]
140
+ details.append(f"Found test directory: {', '.join(matched)}.")
141
+ else:
142
+ details.append("No conventional test directory found (tests/, test/, __tests__/, spec/).")
143
+
144
+ if has_test_config:
145
+ score += 25
146
+ matched = [f for f in files if f in [h.lower() for h in TEST_CONFIG_HINTS]]
147
+ details.append(f"Found test config: {', '.join(matched)}.")
148
+
149
+ # package.json may declare a test script
150
+ pkg = client.file_text(ref, "package.json")
151
+ if pkg and '"test"' in pkg and "no test specified" not in pkg.lower():
152
+ score += 15
153
+ details.append("package.json declares a test script.")
154
+
155
+ if score == 0:
156
+ details.append("No evidence of automated testing.")
157
+
158
+ return _result("Test Presence", score, details)
159
+
160
+
161
+ def check_dependencies(client: GitHubClient, ref: RepoRef) -> CheckResult:
162
+ files = client.root_files(ref)
163
+ found = [f for f in DEPENDENCY_FILES if f in files]
164
+
165
+ if not found:
166
+ return _result("Dependency Hygiene", 50, ["No standard dependency manifest found — may be dependency-free or non-standard layout."])
167
+
168
+ details = [f"Found manifest(s): {', '.join(found)}."]
169
+ score = 70 # baseline for having a manifest
170
+
171
+ # Check for pinned vs unpinned versions as a rough hygiene signal
172
+ if "requirements.txt" in found:
173
+ text = client.file_text(ref, "requirements.txt") or ""
174
+ lines = [l for l in text.splitlines() if l.strip() and not l.startswith("#")]
175
+ pinned = [l for l in lines if "==" in l]
176
+ if lines:
177
+ pin_ratio = len(pinned) / len(lines)
178
+ if pin_ratio > 0.7:
179
+ score += 15
180
+ details.append(f"{len(pinned)}/{len(lines)} dependencies are version-pinned.")
181
+ else:
182
+ details.append(f"Only {len(pinned)}/{len(lines)} dependencies are version-pinned — consider pinning for reproducibility.")
183
+
184
+ if "package.json" in found:
185
+ text = client.file_text(ref, "package.json") or ""
186
+ if '"dependencies"' in text or '"devDependencies"' in text:
187
+ score += 10
188
+ details.append("package.json declares dependencies.")
189
+ if "lock" in [f.lower() for f in files] or "package-lock.json" in files or "yarn.lock" in files:
190
+ score += 10
191
+ details.append("Lockfile present (reproducible installs).")
192
+ else:
193
+ details.append("No lockfile found (package-lock.json / yarn.lock) — installs may not be reproducible.")
194
+
195
+ return _result("Dependency Hygiene", score, details)
196
+
197
+
198
+ def check_license(client: GitHubClient, ref: RepoRef) -> CheckResult:
199
+ repo_data = client.repo(ref)
200
+ license_info = repo_data.get("license")
201
+ if license_info:
202
+ return _result("License", 100, [f"Licensed under {license_info.get('name', license_info.get('spdx_id'))}."])
203
+
204
+ files = client.root_files(ref)
205
+ license_file = next((f for f in files if f.lower().startswith("license")), None)
206
+ if license_file:
207
+ return _result("License", 80, [f"License file present ({license_file}) but not detected by GitHub's API."])
208
+
209
+ return _result("License", 0, ["No license found. Repo is effectively 'all rights reserved' by default."])
210
+
211
+
212
+ def check_activity(client: GitHubClient, ref: RepoRef) -> CheckResult:
213
+ commits = client.commits(ref, per_page=30)
214
+ if not commits:
215
+ return _result("Commit Activity", 0, ["No commit history accessible."])
216
+
217
+ details = []
218
+ score = 0
219
+
220
+ try:
221
+ latest_date_str = commits[0]["commit"]["committer"]["date"]
222
+ latest_date = datetime.fromisoformat(latest_date_str.replace("Z", "+00:00"))
223
+ days_since = (datetime.now(timezone.utc) - latest_date).days
224
+ details.append(f"Last commit {days_since} day(s) ago.")
225
+ if days_since <= 30:
226
+ score += 50
227
+ elif days_since <= 180:
228
+ score += 30
229
+ elif days_since <= 365:
230
+ score += 15
231
+ else:
232
+ details.append("Repo appears inactive (no commits in over a year).")
233
+ except (KeyError, ValueError):
234
+ pass
235
+
236
+ # Commit message quality — rough heuristic: average length, non-trivial messages
237
+ messages = [c["commit"]["message"].splitlines()[0] for c in commits if c.get("commit")]
238
+ trivial = sum(1 for m in messages if m.strip().lower() in ("fix", "update", "wip", "test", "asdf", "."))
239
+ avg_len = sum(len(m) for m in messages) / len(messages) if messages else 0
240
+
241
+ if avg_len >= 20:
242
+ score += 30
243
+ details.append(f"Commit messages average {avg_len:.0f} chars — reasonably descriptive.")
244
+ else:
245
+ details.append(f"Commit messages average {avg_len:.0f} chars — consider more descriptive messages.")
246
+
247
+ if trivial / max(len(messages), 1) > 0.3:
248
+ details.append(f"{trivial}/{len(messages)} recent commits have low-effort messages (e.g. 'fix', 'wip').")
249
+ else:
250
+ score += 20
251
+
252
+ return _result("Commit Activity", score, details)
253
+
254
+
255
+ def check_issue_hygiene(client: GitHubClient, ref: RepoRef) -> CheckResult:
256
+ repo_data = client.repo(ref)
257
+ open_issues = repo_data.get("open_issues_count", 0)
258
+
259
+ if open_issues == 0:
260
+ return _result("Issue Hygiene", 100, ["No open issues — clean backlog (or issues disabled)."])
261
+
262
+ issues = client.issues(ref, state="open", per_page=50)
263
+ # Filter out PRs, which the issues endpoint includes
264
+ real_issues = [i for i in issues if "pull_request" not in i]
265
+
266
+ if not real_issues:
267
+ return _result("Issue Hygiene", 90, [f"{open_issues} open issue(s) reported, but none returned detail (possibly all PRs)."])
268
+
269
+ now = datetime.now(timezone.utc)
270
+ stale = 0
271
+ for issue in real_issues:
272
+ created = datetime.fromisoformat(issue["created_at"].replace("Z", "+00:00"))
273
+ if (now - created).days > 90:
274
+ stale += 1
275
+
276
+ stale_ratio = stale / len(real_issues)
277
+ score = int(100 - (stale_ratio * 80))
278
+ details = [
279
+ f"{open_issues} open issue(s) total.",
280
+ f"{stale}/{len(real_issues)} sampled issues are older than 90 days.",
281
+ ]
282
+ return _result("Issue Hygiene", score, details)
283
+
284
+
285
+ CHECKS: list[Callable[[GitHubClient, RepoRef], CheckResult]] = [
286
+ check_readme,
287
+ check_ci,
288
+ check_tests,
289
+ check_dependencies,
290
+ check_license,
291
+ check_activity,
292
+ check_issue_hygiene,
293
+ ]
repocheck/cli.py ADDED
@@ -0,0 +1,114 @@
1
+ """repocheck CLI."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sys
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+ from rich.text import Text
12
+
13
+ from .github_client import GitHubClient, GitHubError, parse_repo_arg
14
+ from .report import run_report
15
+
16
+ console = Console()
17
+
18
+ GRADE_COLORS = {
19
+ "A": "green",
20
+ "B": "cyan",
21
+ "C": "yellow",
22
+ "D": "orange3",
23
+ "F": "red",
24
+ }
25
+
26
+
27
+ def _grade_text(grade: str) -> Text:
28
+ color = GRADE_COLORS.get(grade, "white")
29
+ return Text(grade, style=f"bold {color}")
30
+
31
+
32
+ @click.command()
33
+ @click.argument("repo")
34
+ @click.option("--token", envvar="GITHUB_TOKEN", help="GitHub personal access token (or set GITHUB_TOKEN env var).")
35
+ @click.option("--json", "as_json", is_flag=True, help="Output raw JSON instead of a formatted report.")
36
+ @click.option("--verbose", "-v", is_flag=True, help="Show detailed reasoning for every check.")
37
+ @click.version_option()
38
+ def main(repo: str, token: str | None, as_json: bool, verbose: bool):
39
+ """Scan a GitHub REPO (owner/repo or URL) and print a health report card.
40
+
41
+ \b
42
+ Examples:
43
+ repocheck pallets/flask
44
+ repocheck https://github.com/psf/requests
45
+ repocheck torvalds/linux --json
46
+ """
47
+ try:
48
+ ref = parse_repo_arg(repo)
49
+ except ValueError as e:
50
+ console.print(f"[bold red]Error:[/bold red] {e}")
51
+ sys.exit(1)
52
+
53
+ client = GitHubClient(token=token)
54
+
55
+ try:
56
+ with console.status(f"[bold blue]Scanning {ref.full_name}..."):
57
+ report = run_report(client, ref)
58
+ except GitHubError as e:
59
+ console.print(f"[bold red]Error:[/bold red] {e}")
60
+ sys.exit(1)
61
+
62
+ if as_json:
63
+ print(json.dumps(report.to_dict(), indent=2))
64
+ return
65
+
66
+ _render(report, verbose=verbose)
67
+
68
+
69
+ def _render(report, verbose: bool):
70
+ grade_color = GRADE_COLORS.get(report.overall_grade, "white")
71
+ header = Text()
72
+ header.append(f"{report.repo.full_name}\n", style="bold")
73
+ header.append(f"★ {report.repo_meta.get('stargazers_count', 0)} ", style="dim")
74
+ header.append(f"⑂ {report.repo_meta.get('forks_count', 0)} ", style="dim")
75
+ header.append(f"{report.repo_meta.get('language') or 'unknown'}", style="dim")
76
+
77
+ score_text = Text()
78
+ score_text.append(f"{report.overall_score}/100 ", style=f"bold {grade_color}")
79
+ score_text.append(f"({report.overall_grade})", style=f"bold {grade_color}")
80
+
81
+ console.print(Panel(header, title="repo", expand=False))
82
+ console.print(Panel(score_text, title="overall health score", expand=False))
83
+
84
+ table = Table(show_header=True, header_style="bold")
85
+ table.add_column("Check")
86
+ table.add_column("Score", justify="right")
87
+ table.add_column("Grade", justify="center")
88
+ if verbose:
89
+ table.add_column("Details")
90
+
91
+ for check in report.checks:
92
+ row = [check.name, str(check.score), _grade_text(check.grade)]
93
+ if verbose:
94
+ row.append("\n".join(f"• {d}" for d in check.details))
95
+ table.add_row(*row)
96
+
97
+ console.print(table)
98
+
99
+ if not verbose:
100
+ console.print("[dim]Run with -v for detailed reasoning behind each score.[/dim]")
101
+
102
+ if client_rate_hint(report):
103
+ console.print(
104
+ "[dim]Tip: set GITHUB_TOKEN to raise your API rate limit from 60/hr to 5000/hr.[/dim]"
105
+ )
106
+
107
+
108
+ def client_rate_hint(report) -> bool:
109
+ import os
110
+ return not os.environ.get("GITHUB_TOKEN")
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()
@@ -0,0 +1,125 @@
1
+ """Thin GitHub REST API client used by all checks."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import os
6
+ import re
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Optional
9
+
10
+ import requests
11
+
12
+ GITHUB_API = "https://api.github.com"
13
+
14
+
15
+ class GitHubError(Exception):
16
+ pass
17
+
18
+
19
+ @dataclass
20
+ class RepoRef:
21
+ owner: str
22
+ name: str
23
+
24
+ @property
25
+ def full_name(self) -> str:
26
+ return f"{self.owner}/{self.name}"
27
+
28
+
29
+ def parse_repo_arg(arg: str) -> RepoRef:
30
+ """Accepts 'owner/repo', a full GitHub URL, or a .git URL."""
31
+ arg = arg.strip()
32
+
33
+ # owner/repo shorthand
34
+ if re.fullmatch(r"[\w.-]+/[\w.-]+", arg):
35
+ owner, name = arg.split("/", 1)
36
+ return RepoRef(owner, name.removesuffix(".git"))
37
+
38
+ # full URL
39
+ m = re.search(r"github\.com[:/]+([\w.-]+)/([\w.-]+?)(?:\.git)?/?$", arg)
40
+ if m:
41
+ return RepoRef(m.group(1), m.group(2))
42
+
43
+ raise ValueError(
44
+ f"Could not parse repo argument: {arg!r}. "
45
+ "Use 'owner/repo' or a github.com URL."
46
+ )
47
+
48
+
49
+ class GitHubClient:
50
+ def __init__(self, token: Optional[str] = None):
51
+ self.token = token or os.environ.get("GITHUB_TOKEN")
52
+ self.session = requests.Session()
53
+ headers = {
54
+ "Accept": "application/vnd.github+json",
55
+ "User-Agent": "repocheck-cli",
56
+ "X-GitHub-Api-Version": "2022-11-28",
57
+ }
58
+ if self.token:
59
+ headers["Authorization"] = f"Bearer {self.token}"
60
+ self.session.headers.update(headers)
61
+ self.rate_remaining: Optional[int] = None
62
+
63
+ def _get(self, path: str, **kwargs) -> requests.Response:
64
+ resp = self.session.get(f"{GITHUB_API}{path}", **kwargs)
65
+ self.rate_remaining = resp.headers.get("X-RateLimit-Remaining")
66
+ if resp.status_code == 404:
67
+ raise GitHubError(f"Not found: {path}")
68
+ if resp.status_code == 403 and self.rate_remaining == "0":
69
+ raise GitHubError(
70
+ "GitHub API rate limit exceeded. Set GITHUB_TOKEN env var "
71
+ "for a higher limit (60/hr -> 5000/hr)."
72
+ )
73
+ resp.raise_for_status()
74
+ return resp
75
+
76
+ def repo(self, ref: RepoRef) -> dict[str, Any]:
77
+ return self._get(f"/repos/{ref.full_name}").json()
78
+
79
+ def contents(self, ref: RepoRef, path: str = "") -> Any:
80
+ try:
81
+ return self._get(f"/repos/{ref.full_name}/contents/{path}").json()
82
+ except GitHubError:
83
+ return None
84
+
85
+ def file_text(self, ref: RepoRef, path: str) -> Optional[str]:
86
+ data = self.contents(ref, path)
87
+ if not data or "content" not in data:
88
+ return None
89
+ try:
90
+ return base64.b64decode(data["content"]).decode("utf-8", errors="replace")
91
+ except Exception:
92
+ return None
93
+
94
+ def workflows(self, ref: RepoRef) -> list[dict[str, Any]]:
95
+ data = self.contents(ref, ".github/workflows")
96
+ return data if isinstance(data, list) else []
97
+
98
+ def commits(self, ref: RepoRef, per_page: int = 30) -> list[dict[str, Any]]:
99
+ try:
100
+ return self._get(
101
+ f"/repos/{ref.full_name}/commits", params={"per_page": per_page}
102
+ ).json()
103
+ except GitHubError:
104
+ return []
105
+
106
+ def issues(self, ref: RepoRef, state: str = "open", per_page: int = 50) -> list[dict[str, Any]]:
107
+ try:
108
+ return self._get(
109
+ f"/repos/{ref.full_name}/issues",
110
+ params={"state": state, "per_page": per_page},
111
+ ).json()
112
+ except GitHubError:
113
+ return []
114
+
115
+ def root_files(self, ref: RepoRef) -> list[str]:
116
+ data = self.contents(ref, "")
117
+ if not isinstance(data, list):
118
+ return []
119
+ return [item["name"] for item in data if item["type"] == "file"]
120
+
121
+ def root_dirs(self, ref: RepoRef) -> list[str]:
122
+ data = self.contents(ref, "")
123
+ if not isinstance(data, list):
124
+ return []
125
+ return [item["name"] for item in data if item["type"] == "dir"]
repocheck/report.py ADDED
@@ -0,0 +1,53 @@
1
+ """Aggregates individual CheckResults into an overall report."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+
6
+ from .checks import CHECKS, CheckResult
7
+ from .github_client import GitHubClient, RepoRef
8
+
9
+
10
+ @dataclass
11
+ class Report:
12
+ repo: RepoRef
13
+ checks: list[CheckResult]
14
+ overall_score: int
15
+ overall_grade: str
16
+ repo_meta: dict
17
+
18
+ def to_dict(self) -> dict:
19
+ return {
20
+ "repo": self.repo.full_name,
21
+ "overall_score": self.overall_score,
22
+ "overall_grade": self.overall_grade,
23
+ "stars": self.repo_meta.get("stargazers_count"),
24
+ "forks": self.repo_meta.get("forks_count"),
25
+ "language": self.repo_meta.get("language"),
26
+ "checks": [
27
+ {
28
+ "name": c.name,
29
+ "score": c.score,
30
+ "grade": c.grade,
31
+ "details": c.details,
32
+ }
33
+ for c in self.checks
34
+ ],
35
+ }
36
+
37
+
38
+ def run_report(client: GitHubClient, ref: RepoRef) -> Report:
39
+ repo_meta = client.repo(ref)
40
+ results = [check_fn(client, ref) for check_fn in CHECKS]
41
+
42
+ total_weight = sum(r.weight for r in results)
43
+ weighted_sum = sum(r.score * r.weight for r in results)
44
+ overall_score = round(weighted_sum / total_weight) if total_weight else 0
45
+ overall_grade = CheckResult.grade_for(overall_score)
46
+
47
+ return Report(
48
+ repo=ref,
49
+ checks=results,
50
+ overall_score=overall_score,
51
+ overall_grade=overall_grade,
52
+ repo_meta=repo_meta,
53
+ )
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: repocheck-cli
3
+ Version: 0.1.0
4
+ Summary: Scan any GitHub repo and get an instant engineering health report card.
5
+ Author: Syed Ahmed Ali
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Ahmed0754/repocheck
8
+ Project-URL: Repository, https://github.com/Ahmed0754/repocheck
9
+ Project-URL: Issues, https://github.com/Ahmed0754/repocheck/issues
10
+ Keywords: github,cli,code-quality,devtools,ci
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Environment :: Console
14
+ Classifier: Topic :: Software Development :: Quality Assurance
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: click>=8.1
19
+ Requires-Dist: requests>=2.31
20
+ Requires-Dist: rich>=13.7
21
+ Dynamic: license-file
22
+
23
+ # repocheck
24
+
25
+ Scan any public GitHub repo and get an instant engineering health report card — README quality, CI/CD, test presence, dependency hygiene, license, commit activity, and issue hygiene, scored A–F.
26
+
27
+ ```
28
+ $ repocheck pallets/flask
29
+
30
+ ╭─ repo ──────────────────╮
31
+ │ pallets/flask │
32
+ │ ★ 68234 ⑂ 16200 Python│
33
+ ╰──────────────────────────╯
34
+ ╭─ overall health score ──╮
35
+ │ 91/100 (A) │
36
+ ╰───────────────────────────╯
37
+
38
+ Check Score Grade
39
+ README Quality 100 A
40
+ CI/CD 90 A
41
+ Test Presence 85 B
42
+ Dependency Hygiene 80 B
43
+ License 100 A
44
+ Commit Activity 95 A
45
+ Issue Hygiene 85 B
46
+ ```
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install repocheck-cli
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ```bash
57
+ # owner/repo shorthand
58
+ repocheck pallets/flask
59
+
60
+ # full URL also works
61
+ repocheck https://github.com/psf/requests
62
+
63
+ # detailed reasoning behind every score
64
+ repocheck torvalds/linux -v
65
+
66
+ # machine-readable output (for CI pipelines, scripts)
67
+ repocheck your-org/your-repo --json
68
+ ```
69
+
70
+ ### Higher rate limits
71
+
72
+ GitHub's anonymous API limit is 60 requests/hour. `repocheck` makes ~8 requests per scan, so you'll hit that fast without auth. Set a token to bump it to 5,000/hour:
73
+
74
+ ```bash
75
+ export GITHUB_TOKEN=ghp_your_token_here
76
+ repocheck your-org/your-repo
77
+ ```
78
+
79
+ No special scopes needed — a basic personal access token works fine for public repos.
80
+
81
+ ## What it checks
82
+
83
+ | Check | What it looks at |
84
+ |---|---|
85
+ | **README Quality** | Presence, length, install/usage sections, badges, code blocks |
86
+ | **CI/CD** | GitHub Actions workflows, legacy CI configs |
87
+ | **Test Presence** | Test directories, test config files, declared test scripts |
88
+ | **Dependency Hygiene** | Manifest presence, version pinning, lockfiles |
89
+ | **License** | OSS license presence and type |
90
+ | **Commit Activity** | Recency and message quality of recent commits |
91
+ | **Issue Hygiene** | Open issue count and staleness |
92
+
93
+ Each check is scored 0–100 and the overall score is an average across all seven.
94
+
95
+ ## Why
96
+
97
+ Most "is this repo any good" judgments are vibes-based — a glance at stars, maybe the README. `repocheck` turns that into a repeatable, objective-ish checklist you can run on your own repos before sharing them, or use to quickly evaluate a dependency before adopting it.
98
+
99
+ ## Local development
100
+
101
+ ```bash
102
+ git clone https://github.com/Ahmed0754/repocheck
103
+ cd repocheck
104
+ pip install -e ".[dev]"
105
+ repocheck pallets/flask
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,11 @@
1
+ repocheck/__init__.py,sha256=ioHB9di2vpZ-uMKhsNwHJ0o05uYtGUBfVepjf12btao,92
2
+ repocheck/checks.py,sha256=-KltRG1fqGmEKWM21h38U1c3FK2iFUPGrtPHoqEPDOg,10601
3
+ repocheck/cli.py,sha256=XAQfW0RySpPplsATX6PHZXQBsa07UMNSRcoIUcOqSnw,3468
4
+ repocheck/github_client.py,sha256=EAkKOT5TEYZG3tj4NROs5bK-lLtWA0EGlMB_QLKi4Ys,4017
5
+ repocheck/report.py,sha256=74WJ0RbKdnRgyv1dPa8bu2PC3MhW9sZx2tU20OcqNWg,1585
6
+ repocheck_cli-0.1.0.dist-info/licenses/LICENSE,sha256=fFUCpT5YzKvFPOpBymHqSaJfHExRYflRFHDFsTVtbCs,1071
7
+ repocheck_cli-0.1.0.dist-info/METADATA,sha256=Tw43yNdNuz4QJi-WP90ukRPFcTyi9yHwcG01BN8Y6I0,3463
8
+ repocheck_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ repocheck_cli-0.1.0.dist-info/entry_points.txt,sha256=TXgW4Wk1w38K_cZm_uTQ9_QQ1Mcr47PM1yiBAJsdt94,49
10
+ repocheck_cli-0.1.0.dist-info/top_level.txt,sha256=cTQiudXFcEYKuHu97745vL874YH0qQbMlq4YnzKq45c,10
11
+ repocheck_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ repocheck = repocheck.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Syed Ahmed Ali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ repocheck