repodoctor-cli 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.
repodoctor/spinner.py ADDED
@@ -0,0 +1,174 @@
1
+ """
2
+ spinner.py — Zero-dependency terminal spinner / progress animation.
3
+
4
+ Uses only Python stdlib: sys, threading, time, itertools.
5
+ Automatically suppresses output when stdout is not a TTY
6
+ (e.g. file redirection, CI pipelines).
7
+ """
8
+
9
+ import sys
10
+ import threading
11
+ import time
12
+ import itertools
13
+
14
+
15
+ # Braille spinner frames — visually smooth, widely supported
16
+ _SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
17
+
18
+ # ANSI colour codes (disabled when not TTY)
19
+ _CYAN = "\033[36m"
20
+ _GREEN = "\033[32m"
21
+ _YELLOW = "\033[33m"
22
+ _RESET = "\033[0m"
23
+ _BOLD = "\033[1m"
24
+
25
+
26
+ def _is_tty() -> bool:
27
+ """Return True only when stdout is an interactive terminal."""
28
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
29
+
30
+
31
+ class Spinner:
32
+ """
33
+ Context-manager / manual spinner that renders a live animation line.
34
+
35
+ Usage (context manager — recommended):
36
+ with Spinner("Scanning repository"):
37
+ do_work()
38
+
39
+ Usage (manual):
40
+ sp = Spinner("Cloning repo")
41
+ sp.start()
42
+ do_work()
43
+ sp.stop(success=True)
44
+ """
45
+
46
+ def __init__(self, message: str = "Working", colour: bool = True, silent: bool = False) -> None:
47
+ self._message = message
48
+ self._use_colour = colour and _is_tty()
49
+ self._active = False
50
+ self._thread: threading.Thread | None = None
51
+ self.silent = silent
52
+
53
+ # ------------------------------------------------------------------ #
54
+ # public API
55
+ # ------------------------------------------------------------------ #
56
+
57
+ def start(self) -> "Spinner":
58
+ if self.silent:
59
+ return self
60
+ if not _is_tty():
61
+ # Non-interactive: just print the static message
62
+ print(f"{self._message}…", flush=True)
63
+ return self
64
+ self._active = True
65
+ self._thread = threading.Thread(target=self._spin, daemon=True)
66
+ self._thread.start()
67
+ return self
68
+
69
+ def stop(self, success: bool = True, final_message: str = "") -> None:
70
+ if self.silent:
71
+ return
72
+ self._active = False
73
+ if self._thread is not None:
74
+ self._thread.join()
75
+ self._thread = None
76
+ if _is_tty():
77
+ # Clear the spinner line
78
+ sys.stdout.write("\r\033[K")
79
+ sys.stdout.flush()
80
+ # Print final status line
81
+ if not final_message:
82
+ final_message = self._message
83
+ if success:
84
+ icon = f"{_GREEN}✔{_RESET}" if self._use_colour else "✔"
85
+ else:
86
+ icon = f"{_YELLOW}✘{_RESET}" if self._use_colour else "✘"
87
+ print(f"{icon} {final_message}", flush=True)
88
+
89
+ def update_message(self, message: str) -> None:
90
+ """Change the message shown next to the spinner in real time."""
91
+ self._message = message
92
+
93
+ # ------------------------------------------------------------------ #
94
+ # context-manager support
95
+ # ------------------------------------------------------------------ #
96
+
97
+ def __enter__(self) -> "Spinner":
98
+ return self.start()
99
+
100
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
101
+ self.stop(success=exc_type is None)
102
+ return False # do not suppress exceptions
103
+
104
+ # ------------------------------------------------------------------ #
105
+ # internal spin loop (runs in background thread)
106
+ # ------------------------------------------------------------------ #
107
+
108
+ def _spin(self) -> None:
109
+ spinner = itertools.cycle(_SPINNER_FRAMES)
110
+ while self._active:
111
+ frame = next(spinner)
112
+ if self._use_colour:
113
+ line = f"\r{_CYAN}{_BOLD}{frame}{_RESET} {self._message} "
114
+ else:
115
+ line = f"\r{frame} {self._message} "
116
+ sys.stdout.write(line)
117
+ sys.stdout.flush()
118
+ time.sleep(0.08)
119
+ # Final clear is done in stop()
120
+
121
+
122
+ class ProgressBar:
123
+ """
124
+ Simple zero-dependency terminal progress bar.
125
+
126
+ Usage:
127
+ bar = ProgressBar(total=len(files), label="Scanning")
128
+ for f in files:
129
+ process(f)
130
+ bar.advance()
131
+ bar.done()
132
+ """
133
+
134
+ def __init__(self, total: int, label: str = "Progress",
135
+ width: int = 30, colour: bool = True) -> None:
136
+ self._total = max(total, 1)
137
+ self._current = 0
138
+ self._label = label
139
+ self._width = width
140
+ self._use_colour = colour and _is_tty()
141
+ self._tty = _is_tty()
142
+
143
+ def advance(self, n: int = 1) -> None:
144
+ self._current = min(self._current + n, self._total)
145
+ if self._tty:
146
+ self._render()
147
+
148
+ def done(self) -> None:
149
+ self._current = self._total
150
+ if self._tty:
151
+ self._render()
152
+ sys.stdout.write("\n")
153
+ sys.stdout.flush()
154
+
155
+ def _render(self) -> None:
156
+ pct = self._current / self._total
157
+ filled = int(self._width * pct)
158
+ bar_body = "█" * filled + "░" * (self._width - filled)
159
+
160
+ if self._use_colour:
161
+ bar_str = (
162
+ f"\r{_CYAN}{self._label}{_RESET} "
163
+ f"[{_GREEN}{bar_body}{_RESET}] "
164
+ f"{_BOLD}{int(pct * 100):3d}%{_RESET} "
165
+ f"({self._current}/{self._total})"
166
+ )
167
+ else:
168
+ bar_str = (
169
+ f"\r{self._label} [{bar_body}] "
170
+ f"{int(pct * 100):3d}% ({self._current}/{self._total})"
171
+ )
172
+
173
+ sys.stdout.write(bar_str)
174
+ sys.stdout.flush()
@@ -0,0 +1,40 @@
1
+ import os
2
+ from typing import Dict, Any
3
+
4
+ def check_project_structure(root_path: str) -> Dict[str, str]:
5
+ """
6
+ Returns PASS, WARN, FAIL, or NOT APPLICABLE
7
+ """
8
+ results = {
9
+ "README": "WARN",
10
+ ".gitignore": "WARN",
11
+ "Tests": "WARN",
12
+ "LICENSE": "WARN",
13
+ "CI config": "WARN"
14
+ }
15
+
16
+ root = os.path.abspath(root_path)
17
+
18
+ # Check README
19
+ if any(os.path.exists(os.path.join(root, f)) for f in ["README.md", "README.txt", "README"]):
20
+ results["README"] = "PASS"
21
+
22
+ # Check .gitignore
23
+ if os.path.exists(os.path.join(root, ".gitignore")):
24
+ results[".gitignore"] = "PASS"
25
+ elif not os.path.exists(os.path.join(root, ".git")):
26
+ results[".gitignore"] = "NOT APPLICABLE"
27
+
28
+ # Check tests
29
+ if os.path.exists(os.path.join(root, "tests")) or os.path.exists(os.path.join(root, "test")):
30
+ results["Tests"] = "PASS"
31
+
32
+ # Check LICENSE
33
+ if any(os.path.exists(os.path.join(root, f)) for f in ["LICENSE", "LICENSE.txt", "LICENSE.md"]):
34
+ results["LICENSE"] = "PASS"
35
+
36
+ # Check CI config
37
+ if os.path.exists(os.path.join(root, ".github")) or os.path.exists(os.path.join(root, ".gitlab-ci.yml")):
38
+ results["CI config"] = "PASS"
39
+
40
+ return results
repodoctor/todos.py ADDED
@@ -0,0 +1,32 @@
1
+ import re
2
+ from typing import List
3
+ from .models import FileInfo, TodoItem
4
+
5
+ MARKERS = ["TODO", "FIXME", "HACK", "XXX", "BUG"]
6
+ MARKER_PATTERN = re.compile(r'\b(' + '|'.join(MARKERS) + r')\b')
7
+
8
+ def scan_todos(files: List[FileInfo]) -> List[TodoItem]:
9
+ todos = []
10
+
11
+ for f in files:
12
+ if f.is_binary:
13
+ continue
14
+
15
+ try:
16
+ with open(f.path, 'r', encoding='utf-8', errors='ignore') as file:
17
+ for line_idx, line in enumerate(file):
18
+ if MARKER_PATTERN.search(line):
19
+ # Extract the actual marker used
20
+ match = MARKER_PATTERN.search(line)
21
+ marker = match.group(1)
22
+
23
+ todos.append(TodoItem(
24
+ filepath=f.relative_path,
25
+ line_number=line_idx + 1,
26
+ text=line.strip(),
27
+ marker=marker
28
+ ))
29
+ except Exception:
30
+ pass
31
+
32
+ return todos
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: repodoctor-cli
3
+ Version: 1.0.0
4
+ Summary: Zero-dependency repository health analyser
5
+ Author: Tanish Jain, Harsh Kumawat
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Quality Assurance
11
+ Classifier: Environment :: Console
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Dynamic: requires-python
15
+
16
+ # RepoDoctor
17
+
18
+ > RepoDoctor diagnoses a codebase for maintainability, security, duplication, project-structure and Git issues using only the language standard library.
19
+
20
+ ## Problem
21
+ Modern development tools rely on heavy dependency chains that are hard to audit, difficult to install in restricted environments, and prone to breaking changes.
22
+
23
+ ## Solution
24
+ RepoDoctor is a production-quality CLI tool that analyzes a software repository and provides an actionable health, security, and maintainability report without a single third-party runtime dependency.
25
+
26
+ ## Features
27
+ - **Multi-Threaded Parallel Scanning**: Asynchronously processes massive codebases in milliseconds.\n- **Animated Terminal UI**: Beautiful typewriter animations and progress spinners.\n- **Multi-Repository Aggregation**: Scan multiple codebases simultaneously and generate unified or independent reports across all flags (HTML, JSON, LLM prompt).\n- **Zero Runtime Dependencies**: Built entirely with Python's standard library.
28
+ - **Single-File Portability**: Can be compiled into a single `repodoctor_single.py` script for extreme portability.
29
+ - **Developer Mood Analyzer**: Scans code comments and commit messages to calculate the emotional state of the project team.
30
+ - **Code Clone Exposer**: Mathematically cross-references all files to expose the two most identical copy-pasted files in the project.
31
+ - **Micro-Linter Engine**: Instantly flags 30+ code smells including profanity filters, wildcard imports, massive JSON configs, and missing alt-text.
32
+ - **LLM Prompt Exporter**: Instantly bundle your entire codebase into a single text file ready for ChatGPT/Claude (`--export-prompt`).
33
+ - **SVG Badge Generator**: Generate valid GitHub-style SVG health badges without image processing libraries (`--badge`).
34
+ - **Terminal ASCII Tree & Bar Charts**: Visual breakdown of your project's folders (`--tree`) and languages natively in your terminal.
35
+ - **AST Cyclomatic Complexity**: Parses Python Abstract Syntax Trees mathematically to score code logic complexity.
36
+ - **Security Scanner**: Detects exposed API keys, credentials, and `.env` files and automatically redacts findings.
37
+ - **Git Analytics & Hotspots**: Leverages native Git to report top contributors, uncommitted changes, and your most frequently edited file (Hotspot).
38
+ - **Codebase Vocabulary Cloud**: Automatically extracts the most frequently used variable and function names across all your files.
39
+ - **Rich Output Formats**: Choose between Animated ANSI-colored terminal, HTML (`--html`), or JSON (`--json`).
40
+ - **Baseline Tracking**: Compare current scans against past reports (`--baseline`) to track regressions over time.
41
+
42
+ ## Architecture
43
+ Modular Python architecture utilizing built-in `argparse`, `subprocess`, `ast`, and `unittest`. Data structures rely on `dataclasses`.
44
+
45
+ ## Installation
46
+ No dependencies are required. Clone the repository or copy the `repodoctor` folder:
47
+
48
+ ```bash
49
+ git clone https://github.com/example/repodoctor.git
50
+ ```
51
+
52
+ ## Usage
53
+ Run the package directory against your target repository:
54
+
55
+ ```bash
56
+ python -m repodoctor /path/to/your/repo
57
+ ```
58
+
59
+ ### CLI Options
60
+
61
+ | Flag | Description |
62
+ |---|---|
63
+ | `path` | Path to the repository (default: `.`) |
64
+ | `--json` | Output valid machine-readable JSON |
65
+ | `--html FILE` | Output a self-contained HTML dashboard report |
66
+ | `--export-prompt FILE`| Export the codebase into a single text file for LLM prompting |
67
+ | `--badge FILE` | Generate a GitHub-style SVG health badge |
68
+ | `--tree` | Print an ASCII project directory tree at the top of the report |
69
+ | `--baseline FILE` | Path to a previous JSON report to calculate delta trends |
70
+ | `--no-color` | Disable animated ANSI color output |
71
+ | `--ignore` | Comma-separated list of custom directories to ignore |
72
+ | `--large-file-lines` | Threshold for large file lines (default: 500) |
73
+ | `--duplicate-lines` | Minimum lines for duplicate detection (default: 8) |
74
+ | `--security` | Focus only on security analysis |
75
+ | `--todos` | Focus only on TODO/FIXME analysis |
76
+ | `--git` | Include Git analysis (Always attempts by default) |
77
+ | `--verbose` | Enable verbose logging |
78
+ | `--version` | Display version |
79
+ | `--help` | Display help |
80
+
81
+ ### Exit Codes
82
+ - `0`: Successful scan, no serious findings (secrets or duplicates).
83
+ - `1`: Successful scan with findings.
84
+ - `2`: Invalid CLI usage.
85
+
86
+ ## JSON Format
87
+ Use the `--json` flag to export data.
88
+ ```json
89
+ {
90
+ "repository": { "path": ".", "name": "project" },
91
+ "summary": { "files": 247, "lines": 38421, "health_score": 78 },
92
+ "security": { "potential_secrets": 0, "findings": [] },
93
+ "maintainability": { "large_files": 0, "todos": 5, "duplicates": 0 },
94
+ "git": { "available": true, "branch": "main", "commits": 142, "uncommitted_changes": 0 },
95
+ "structure": { "README": "PASS", "Tests": "PASS" }
96
+ }
97
+ ```
98
+
99
+ ## Performance
100
+ - Uses efficient filesystem walking (`os.walk`).
101
+ - Early bailing on binary files.
102
+ - Rolling window chunking for O(N) deduplication analysis.
103
+
104
+ ## Security Model
105
+ - **Local Only**: No data is uploaded or transmitted.
106
+ - **Redacted Output**: Secrets are never dumped fully in terminal or JSON.
107
+ - **No Evaluation**: Source code is parsed statically (via AST/Regex), never executed.
108
+ - **Safe Execution**: Git commands strictly avoid shell interpolation to prevent injection.
109
+
110
+ ## Limitations
111
+ - Language detection is extension-based.
112
+ - Duplicate detection is line-based rather than AST-based.
113
+ - Security scanner may yield false positives; human review is required.
114
+
115
+ ## Zero-Dependency Proof
116
+ To verify, run within a fully clean virtual environment:
117
+ ```bash
118
+ python -m venv /tmp/repodoctor-test
119
+ source /tmp/repodoctor-test/bin/activate
120
+ pip freeze # (Will be empty)
121
+ python -m repodoctor /path/to/repo1 /path/to/repo2
122
+ ```
123
+
124
+ ## Standard Library Substitutions
125
+ See [STDLIB.md](STDLIB.md) for details on how we substituted common third-party tools.
126
+
127
+ ## Testing
128
+ Tested with Python `unittest`:
129
+ ```bash
130
+ python -m unittest discover -s tests -v
131
+ ```
132
+
133
+ ## Hackathon Information
134
+ Built for the **Zero Dependency | 72-Hour Hackathon**.
135
+
136
+ ## License
137
+ MIT
@@ -0,0 +1,22 @@
1
+ repodoctor/__init__.py,sha256=OEMGSsx5z9zhqMrY1Qiri25mFUKtpCbn3ZUUAbjl1C8,25
2
+ repodoctor/__main__.py,sha256=wMctmzk1VnpPrTtl1gPg2pLCcMI4qqsoacCIpsPiZoM,15140
3
+ repodoctor/baseline.py,sha256=4DF7XlEdoQoyaNO4-7B3RBQ2T_VWco6b6c4Wz3tuv1k,1557
4
+ repodoctor/cli.py,sha256=tk06dGY8pvi_SVuuc3H6fjBPVQOqh4c9AM2_VNjNGRU,2605
5
+ repodoctor/duplicates.py,sha256=ZPc01cEnj0yIscRaK3uKojVCJVZ6huxq2hXV-KISY80,2695
6
+ repodoctor/git.py,sha256=iZRneYlk6Y0vF7UbDjs0xr7u2sIA5MnVyNfMd_OmeMU,2186
7
+ repodoctor/languages.py,sha256=6IE9iFNIS3kfgTPDan6S_m29l0rCmdnSD4rMw4TThb0,695
8
+ repodoctor/linter.py,sha256=8GyufyGE30Y-lRCovkUXj4bMBSBGbgXBO2dDWJGbfKI,10383
9
+ repodoctor/metrics.py,sha256=nLLCcc3SODdJJX5fOfU87nwhTdTisu6dtWwH0NofHxU,2677
10
+ repodoctor/models.py,sha256=wFDFGjZKhkEIe6iDRu5Pv9c4gm7uKR3lftlo1iFRWkE,1626
11
+ repodoctor/report.py,sha256=lei0l-02Iw8BTchUbAEjqz-K55WAZ5sVAVHWWlcOIfA,16090
12
+ repodoctor/scanner.py,sha256=0cm3Cmde17EvpM12qI8cRwZVnRo3meP9Ka5s_ielJH4,5886
13
+ repodoctor/scoring.py,sha256=fIDySAxVmC0yL3UWRjCn5OwkAR6vWEVnRuPUuHEQXeY,1635
14
+ repodoctor/security.py,sha256=R1haBYjB9zZP_u7vWZmkzDvcVkWgfi2sJ1-lrG5BuSE,2521
15
+ repodoctor/spinner.py,sha256=bZ7cPXZnUXSZ_RM4jyrv6S_gKcy-mfffL43AyM63lLY,5670
16
+ repodoctor/structure.py,sha256=ViHaXTUjkBQwJ01kvkcAJ00PyYAhfufgVQCa34H3W2s,1247
17
+ repodoctor/todos.py,sha256=E0xsmGvkjkjrMcO2IdaK7Qwjq5kCmgtV6FSR6rkLIEM,1066
18
+ repodoctor_cli-1.0.0.dist-info/METADATA,sha256=eXATE7ibyE8jnO8IgHf8xJbQn-zwO2CrQEfztJ25Wzc,6641
19
+ repodoctor_cli-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
20
+ repodoctor_cli-1.0.0.dist-info/entry_points.txt,sha256=c51JinAxRwn80raaW9iGVHnbQyqQUXwyiVKtTuEch9k,56
21
+ repodoctor_cli-1.0.0.dist-info/top_level.txt,sha256=lheErZ2yZxbqhKX-I6kK39b8IOHHU24X4dLI1H252Gg,11
22
+ repodoctor_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ repodoctor = repodoctor.__main__:main
@@ -0,0 +1 @@
1
+ repodoctor