repodoctor-cli 1.0.0__tar.gz
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_cli-1.0.0/PKG-INFO +137 -0
- repodoctor_cli-1.0.0/README.md +122 -0
- repodoctor_cli-1.0.0/pyproject.toml +30 -0
- repodoctor_cli-1.0.0/repodoctor/__init__.py +1 -0
- repodoctor_cli-1.0.0/repodoctor/__main__.py +370 -0
- repodoctor_cli-1.0.0/repodoctor/baseline.py +42 -0
- repodoctor_cli-1.0.0/repodoctor/cli.py +47 -0
- repodoctor_cli-1.0.0/repodoctor/duplicates.py +69 -0
- repodoctor_cli-1.0.0/repodoctor/git.py +67 -0
- repodoctor_cli-1.0.0/repodoctor/languages.py +33 -0
- repodoctor_cli-1.0.0/repodoctor/linter.py +226 -0
- repodoctor_cli-1.0.0/repodoctor/metrics.py +73 -0
- repodoctor_cli-1.0.0/repodoctor/models.py +81 -0
- repodoctor_cli-1.0.0/repodoctor/report.py +361 -0
- repodoctor_cli-1.0.0/repodoctor/scanner.py +177 -0
- repodoctor_cli-1.0.0/repodoctor/scoring.py +49 -0
- repodoctor_cli-1.0.0/repodoctor/security.py +56 -0
- repodoctor_cli-1.0.0/repodoctor/spinner.py +174 -0
- repodoctor_cli-1.0.0/repodoctor/structure.py +40 -0
- repodoctor_cli-1.0.0/repodoctor/todos.py +32 -0
- repodoctor_cli-1.0.0/repodoctor_cli.egg-info/PKG-INFO +137 -0
- repodoctor_cli-1.0.0/repodoctor_cli.egg-info/SOURCES.txt +38 -0
- repodoctor_cli-1.0.0/repodoctor_cli.egg-info/dependency_links.txt +1 -0
- repodoctor_cli-1.0.0/repodoctor_cli.egg-info/entry_points.txt +2 -0
- repodoctor_cli-1.0.0/repodoctor_cli.egg-info/top_level.txt +1 -0
- repodoctor_cli-1.0.0/setup.cfg +4 -0
- repodoctor_cli-1.0.0/setup.py +35 -0
- repodoctor_cli-1.0.0/tests/test_baseline.py +60 -0
- repodoctor_cli-1.0.0/tests/test_cli.py +28 -0
- repodoctor_cli-1.0.0/tests/test_duplicates.py +38 -0
- repodoctor_cli-1.0.0/tests/test_git.py +42 -0
- repodoctor_cli-1.0.0/tests/test_languages.py +14 -0
- repodoctor_cli-1.0.0/tests/test_metrics.py +32 -0
- repodoctor_cli-1.0.0/tests/test_packaging.py +108 -0
- repodoctor_cli-1.0.0/tests/test_parallel.py +93 -0
- repodoctor_cli-1.0.0/tests/test_scanner.py +44 -0
- repodoctor_cli-1.0.0/tests/test_scoring.py +31 -0
- repodoctor_cli-1.0.0/tests/test_security.py +37 -0
- repodoctor_cli-1.0.0/tests/test_structure.py +38 -0
- repodoctor_cli-1.0.0/tests/test_todos.py +30 -0
|
@@ -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,122 @@
|
|
|
1
|
+
# RepoDoctor
|
|
2
|
+
|
|
3
|
+
> RepoDoctor diagnoses a codebase for maintainability, security, duplication, project-structure and Git issues using only the language standard library.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
Modern development tools rely on heavy dependency chains that are hard to audit, difficult to install in restricted environments, and prone to breaking changes.
|
|
7
|
+
|
|
8
|
+
## Solution
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
- **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.
|
|
13
|
+
- **Single-File Portability**: Can be compiled into a single `repodoctor_single.py` script for extreme portability.
|
|
14
|
+
- **Developer Mood Analyzer**: Scans code comments and commit messages to calculate the emotional state of the project team.
|
|
15
|
+
- **Code Clone Exposer**: Mathematically cross-references all files to expose the two most identical copy-pasted files in the project.
|
|
16
|
+
- **Micro-Linter Engine**: Instantly flags 30+ code smells including profanity filters, wildcard imports, massive JSON configs, and missing alt-text.
|
|
17
|
+
- **LLM Prompt Exporter**: Instantly bundle your entire codebase into a single text file ready for ChatGPT/Claude (`--export-prompt`).
|
|
18
|
+
- **SVG Badge Generator**: Generate valid GitHub-style SVG health badges without image processing libraries (`--badge`).
|
|
19
|
+
- **Terminal ASCII Tree & Bar Charts**: Visual breakdown of your project's folders (`--tree`) and languages natively in your terminal.
|
|
20
|
+
- **AST Cyclomatic Complexity**: Parses Python Abstract Syntax Trees mathematically to score code logic complexity.
|
|
21
|
+
- **Security Scanner**: Detects exposed API keys, credentials, and `.env` files and automatically redacts findings.
|
|
22
|
+
- **Git Analytics & Hotspots**: Leverages native Git to report top contributors, uncommitted changes, and your most frequently edited file (Hotspot).
|
|
23
|
+
- **Codebase Vocabulary Cloud**: Automatically extracts the most frequently used variable and function names across all your files.
|
|
24
|
+
- **Rich Output Formats**: Choose between Animated ANSI-colored terminal, HTML (`--html`), or JSON (`--json`).
|
|
25
|
+
- **Baseline Tracking**: Compare current scans against past reports (`--baseline`) to track regressions over time.
|
|
26
|
+
|
|
27
|
+
## Architecture
|
|
28
|
+
Modular Python architecture utilizing built-in `argparse`, `subprocess`, `ast`, and `unittest`. Data structures rely on `dataclasses`.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
No dependencies are required. Clone the repository or copy the `repodoctor` folder:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/example/repodoctor.git
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
Run the package directory against your target repository:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python -m repodoctor /path/to/your/repo
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### CLI Options
|
|
45
|
+
|
|
46
|
+
| Flag | Description |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `path` | Path to the repository (default: `.`) |
|
|
49
|
+
| `--json` | Output valid machine-readable JSON |
|
|
50
|
+
| `--html FILE` | Output a self-contained HTML dashboard report |
|
|
51
|
+
| `--export-prompt FILE`| Export the codebase into a single text file for LLM prompting |
|
|
52
|
+
| `--badge FILE` | Generate a GitHub-style SVG health badge |
|
|
53
|
+
| `--tree` | Print an ASCII project directory tree at the top of the report |
|
|
54
|
+
| `--baseline FILE` | Path to a previous JSON report to calculate delta trends |
|
|
55
|
+
| `--no-color` | Disable animated ANSI color output |
|
|
56
|
+
| `--ignore` | Comma-separated list of custom directories to ignore |
|
|
57
|
+
| `--large-file-lines` | Threshold for large file lines (default: 500) |
|
|
58
|
+
| `--duplicate-lines` | Minimum lines for duplicate detection (default: 8) |
|
|
59
|
+
| `--security` | Focus only on security analysis |
|
|
60
|
+
| `--todos` | Focus only on TODO/FIXME analysis |
|
|
61
|
+
| `--git` | Include Git analysis (Always attempts by default) |
|
|
62
|
+
| `--verbose` | Enable verbose logging |
|
|
63
|
+
| `--version` | Display version |
|
|
64
|
+
| `--help` | Display help |
|
|
65
|
+
|
|
66
|
+
### Exit Codes
|
|
67
|
+
- `0`: Successful scan, no serious findings (secrets or duplicates).
|
|
68
|
+
- `1`: Successful scan with findings.
|
|
69
|
+
- `2`: Invalid CLI usage.
|
|
70
|
+
|
|
71
|
+
## JSON Format
|
|
72
|
+
Use the `--json` flag to export data.
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"repository": { "path": ".", "name": "project" },
|
|
76
|
+
"summary": { "files": 247, "lines": 38421, "health_score": 78 },
|
|
77
|
+
"security": { "potential_secrets": 0, "findings": [] },
|
|
78
|
+
"maintainability": { "large_files": 0, "todos": 5, "duplicates": 0 },
|
|
79
|
+
"git": { "available": true, "branch": "main", "commits": 142, "uncommitted_changes": 0 },
|
|
80
|
+
"structure": { "README": "PASS", "Tests": "PASS" }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Performance
|
|
85
|
+
- Uses efficient filesystem walking (`os.walk`).
|
|
86
|
+
- Early bailing on binary files.
|
|
87
|
+
- Rolling window chunking for O(N) deduplication analysis.
|
|
88
|
+
|
|
89
|
+
## Security Model
|
|
90
|
+
- **Local Only**: No data is uploaded or transmitted.
|
|
91
|
+
- **Redacted Output**: Secrets are never dumped fully in terminal or JSON.
|
|
92
|
+
- **No Evaluation**: Source code is parsed statically (via AST/Regex), never executed.
|
|
93
|
+
- **Safe Execution**: Git commands strictly avoid shell interpolation to prevent injection.
|
|
94
|
+
|
|
95
|
+
## Limitations
|
|
96
|
+
- Language detection is extension-based.
|
|
97
|
+
- Duplicate detection is line-based rather than AST-based.
|
|
98
|
+
- Security scanner may yield false positives; human review is required.
|
|
99
|
+
|
|
100
|
+
## Zero-Dependency Proof
|
|
101
|
+
To verify, run within a fully clean virtual environment:
|
|
102
|
+
```bash
|
|
103
|
+
python -m venv /tmp/repodoctor-test
|
|
104
|
+
source /tmp/repodoctor-test/bin/activate
|
|
105
|
+
pip freeze # (Will be empty)
|
|
106
|
+
python -m repodoctor /path/to/repo1 /path/to/repo2
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Standard Library Substitutions
|
|
110
|
+
See [STDLIB.md](STDLIB.md) for details on how we substituted common third-party tools.
|
|
111
|
+
|
|
112
|
+
## Testing
|
|
113
|
+
Tested with Python `unittest`:
|
|
114
|
+
```bash
|
|
115
|
+
python -m unittest discover -s tests -v
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Hackathon Information
|
|
119
|
+
Built for the **Zero Dependency | 72-Hour Hackathon**.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# PEP 517 / 518 — build backend is setuptools (stdlib-bundled, zero new installs)
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = ["setuptools>=42", "wheel"]
|
|
4
|
+
build-backend = "setuptools.build_meta"
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "repodoctor-cli"
|
|
8
|
+
version = "1.0.0"
|
|
9
|
+
description = "Zero-dependency repository health analyser"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = [] # ZERO runtime dependencies
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Tanish Jain, Harsh Kumawat" }
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
22
|
+
"Environment :: Console",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
repodoctor = "repodoctor.__main__:main"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["."]
|
|
30
|
+
include = ["repodoctor*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""RepoDoctor package"""
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import re
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
import collections
|
|
7
|
+
import io
|
|
8
|
+
import contextlib
|
|
9
|
+
|
|
10
|
+
# Force utf-8 output to avoid cp1252 encoding errors on Windows
|
|
11
|
+
if sys.stdout.encoding != 'utf-8':
|
|
12
|
+
try:
|
|
13
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
from .cli import parse_args
|
|
18
|
+
from .scanner import scan_repository
|
|
19
|
+
from .languages import detect_languages
|
|
20
|
+
from .metrics import analyze_metrics
|
|
21
|
+
from .todos import scan_todos
|
|
22
|
+
from .security import scan_security
|
|
23
|
+
from .duplicates import scan_duplicates
|
|
24
|
+
from .structure import check_project_structure
|
|
25
|
+
from .git import get_git_info
|
|
26
|
+
from .scoring import calculate_score
|
|
27
|
+
from .report import print_terminal_report, get_json_report, generate_html_report
|
|
28
|
+
from .baseline import compare_baseline
|
|
29
|
+
from .models import ReportData
|
|
30
|
+
from .spinner import Spinner
|
|
31
|
+
|
|
32
|
+
def process_single_repo(root_path, args, idx, custom_ignores, use_parallel, show_animation, start_time):
|
|
33
|
+
repo_start_time = time.time()
|
|
34
|
+
# Determine if we should suppress live spinner output (if scanning multiple repos)
|
|
35
|
+
silent = len(args.path) > 1
|
|
36
|
+
|
|
37
|
+
# 1. Scan files
|
|
38
|
+
files = scan_repository(
|
|
39
|
+
root_path,
|
|
40
|
+
custom_ignores,
|
|
41
|
+
parallel=use_parallel,
|
|
42
|
+
show_animation=show_animation and not silent,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# 2. Run analysis phases
|
|
46
|
+
with Spinner(f"Detecting languages ({root_path})", colour=show_animation, silent=silent):
|
|
47
|
+
detect_languages(files)
|
|
48
|
+
|
|
49
|
+
with Spinner(f"Analysing metrics ({root_path})", colour=show_animation, silent=silent):
|
|
50
|
+
analyze_metrics(files)
|
|
51
|
+
|
|
52
|
+
with Spinner(f"Scanning TODOs ({root_path})", colour=show_animation, silent=silent):
|
|
53
|
+
todos = scan_todos(files)
|
|
54
|
+
|
|
55
|
+
with Spinner(f"Scanning security patterns ({root_path})", colour=show_animation, silent=silent):
|
|
56
|
+
security = scan_security(files)
|
|
57
|
+
|
|
58
|
+
with Spinner(f"Detecting duplicates ({root_path})", colour=show_animation, silent=silent):
|
|
59
|
+
duplicates = scan_duplicates(files, args.duplicate_lines)
|
|
60
|
+
|
|
61
|
+
with Spinner(f"Checking project structure ({root_path})", colour=show_animation, silent=silent):
|
|
62
|
+
structure = check_project_structure(root_path)
|
|
63
|
+
|
|
64
|
+
with Spinner(f"Reading Git info ({root_path})", colour=show_animation, silent=silent):
|
|
65
|
+
git_info = get_git_info(root_path)
|
|
66
|
+
|
|
67
|
+
repo_name = os.path.basename(os.path.abspath(root_path)) or "Unknown"
|
|
68
|
+
|
|
69
|
+
# AI & Advanced analytics (computed just in time)
|
|
70
|
+
all_words = []
|
|
71
|
+
for f in files:
|
|
72
|
+
try:
|
|
73
|
+
with open(f.path, 'r', encoding='utf-8', errors='ignore') as file_handle:
|
|
74
|
+
content = file_handle.read()
|
|
75
|
+
f._words = re.findall(r'\b[a-zA-Z_]{3,}\b', content)
|
|
76
|
+
all_words.extend(f._words)
|
|
77
|
+
except Exception:
|
|
78
|
+
f._words = []
|
|
79
|
+
|
|
80
|
+
positive_words = {"awesome", "great", "excellent", "amazing", "good", "perfect", "wow", "love", "thanks", "beautiful", "brilliant", "clean", "elegant", "smart"}
|
|
81
|
+
negative_words = {"fuck", "shit", "crap", "bitch", "damn", "hate", "ugly", "stupid", "terrible", "awful", "horrible", "mess", "hack", "fixme", "gross", "disgusting", "wtf"}
|
|
82
|
+
|
|
83
|
+
pos_count = sum(1 for f in files for w in getattr(f, "_words", []) if w.lower() in positive_words)
|
|
84
|
+
neg_count = sum(1 for f in files for w in getattr(f, "_words", []) if w.lower() in negative_words)
|
|
85
|
+
|
|
86
|
+
if pos_count == 0 and neg_count == 0:
|
|
87
|
+
mood_str = "Neutral 😐 (0 positive, 0 negative words)"
|
|
88
|
+
elif pos_count > neg_count * 2:
|
|
89
|
+
mood_str = f"Highly Motivated 🚀 ({pos_count} positive, {neg_count} negative words)"
|
|
90
|
+
elif neg_count > pos_count * 2:
|
|
91
|
+
mood_str = f"Severely Frustrated 😡 ({pos_count} positive, {neg_count} negative words)"
|
|
92
|
+
else:
|
|
93
|
+
mood_str = f"Balanced ⚖️ ({pos_count} positive, {neg_count} negative words)"
|
|
94
|
+
|
|
95
|
+
clone_str = "No major clones detected 👏"
|
|
96
|
+
if len(files) > 1:
|
|
97
|
+
try:
|
|
98
|
+
import difflib
|
|
99
|
+
texts = [(f, " ".join(getattr(f, "_words", []))) for f in files if len(getattr(f, "_words", [])) > 50]
|
|
100
|
+
if len(texts) > 1:
|
|
101
|
+
texts.sort(key=lambda x: len(x[1]), reverse=True)
|
|
102
|
+
top_files = texts[:10]
|
|
103
|
+
best_ratio = 0
|
|
104
|
+
best_pair = None
|
|
105
|
+
for i in range(len(top_files)):
|
|
106
|
+
for j in range(i+1, len(top_files)):
|
|
107
|
+
ratio = difflib.SequenceMatcher(None, top_files[i][1], top_files[j][1]).quick_ratio()
|
|
108
|
+
if ratio > best_ratio:
|
|
109
|
+
best_ratio = ratio
|
|
110
|
+
best_pair = (top_files[i][0].path, top_files[j][0].path)
|
|
111
|
+
if best_ratio > 0.8:
|
|
112
|
+
clone_str = f"{best_pair[0]} & {best_pair[1]} ({int(best_ratio*100)}% identical)"
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
stop_words = {"the", "and", "but", "for", "with", "was", "were", "been", "being", "have", "has", "had", "will", "would", "shall", "should", "can", "could", "may", "might", "must", "then", "else", "while", "def", "class", "return", "import", "from", "print", "self", "None", "True", "False"}
|
|
117
|
+
filtered_words = [w for w in all_words if len(w) > 3 and w.lower() not in stop_words]
|
|
118
|
+
top_words = collections.Counter(filtered_words).most_common(5)
|
|
119
|
+
|
|
120
|
+
data = ReportData(
|
|
121
|
+
path=os.path.abspath(root_path),
|
|
122
|
+
name=repo_name,
|
|
123
|
+
files=files,
|
|
124
|
+
todos=todos,
|
|
125
|
+
security=security,
|
|
126
|
+
duplicates=duplicates,
|
|
127
|
+
structure=structure,
|
|
128
|
+
git=git_info,
|
|
129
|
+
score=None
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
data.mood = mood_str
|
|
133
|
+
data.clone_exposer = clone_str
|
|
134
|
+
data.top_words = top_words
|
|
135
|
+
|
|
136
|
+
score = calculate_score(data)
|
|
137
|
+
data.score = score
|
|
138
|
+
|
|
139
|
+
local_exit_code = 0
|
|
140
|
+
if score and score.score < getattr(args, "fail_under", 0):
|
|
141
|
+
local_exit_code = 1
|
|
142
|
+
|
|
143
|
+
deltas = None
|
|
144
|
+
if getattr(args, "baseline", None) and os.path.exists(args.baseline):
|
|
145
|
+
try:
|
|
146
|
+
import json
|
|
147
|
+
with open(args.baseline, "r") as bf:
|
|
148
|
+
base_data = json.load(bf)
|
|
149
|
+
if "score" in base_data and data.score:
|
|
150
|
+
deltas = {"score": data.score.score - base_data["score"]}
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# Generate badge SVG
|
|
155
|
+
badge_svg = None
|
|
156
|
+
badge_path = None
|
|
157
|
+
if getattr(args, "badge", None):
|
|
158
|
+
color = "#4c1" if score.score >= 90 else ("#dfb317" if score.score >= 70 else "#e05d44")
|
|
159
|
+
badge_svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="140" height="20">
|
|
160
|
+
<linearGradient id="b" x2="0" y2="100%">
|
|
161
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
162
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
163
|
+
</linearGradient>
|
|
164
|
+
<mask id="a">
|
|
165
|
+
<rect width="140" height="20" rx="3" fill="#fff"/>
|
|
166
|
+
</mask>
|
|
167
|
+
<g mask="url(#a)">
|
|
168
|
+
<path fill="#555" d="M0 0h80v20H0z"/>
|
|
169
|
+
<path fill="{color}" d="M80 0h60v20H0z"/>
|
|
170
|
+
<path fill="url(#b)" d="M0 0h140v20H0z"/>
|
|
171
|
+
</g>
|
|
172
|
+
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
173
|
+
<text x="40" y="15" fill="#010101" fill-opacity=".3">RepoDoctor</text>
|
|
174
|
+
<text x="40" y="14">RepoDoctor</text>
|
|
175
|
+
<text x="109" y="15" fill="#010101" fill-opacity=".3">{score.score}/100</text>
|
|
176
|
+
<text x="109" y="14">{score.score}/100</text>
|
|
177
|
+
</g>
|
|
178
|
+
</svg>'''
|
|
179
|
+
badge_path = args.badge
|
|
180
|
+
if len(args.path) > 1:
|
|
181
|
+
base, ext = os.path.splitext(badge_path)
|
|
182
|
+
badge_path = f"{base}_{idx+1}{ext}"
|
|
183
|
+
|
|
184
|
+
# Capture terminal report
|
|
185
|
+
terminal_report = ""
|
|
186
|
+
repo_duration = time.time() - repo_start_time
|
|
187
|
+
if not args.json:
|
|
188
|
+
f_buf = io.StringIO()
|
|
189
|
+
with contextlib.redirect_stdout(f_buf):
|
|
190
|
+
use_color = not args.no_color and sys.stdout.isatty()
|
|
191
|
+
print_terminal_report(data, use_color, args.large_file_lines, deltas, repo_duration, getattr(args, 'tree', False))
|
|
192
|
+
terminal_report = f_buf.getvalue()
|
|
193
|
+
|
|
194
|
+
# Generate JSON
|
|
195
|
+
json_report = None
|
|
196
|
+
if args.json:
|
|
197
|
+
from .report import get_json_report
|
|
198
|
+
import json
|
|
199
|
+
json_report = json.loads(get_json_report(data, args.large_file_lines))
|
|
200
|
+
|
|
201
|
+
# Generate HTML
|
|
202
|
+
html_report = None
|
|
203
|
+
if args.html:
|
|
204
|
+
html_report = generate_html_report(data, args.large_file_lines)
|
|
205
|
+
|
|
206
|
+
# Generate LLM Export
|
|
207
|
+
llm_report = None
|
|
208
|
+
if args.export_prompt:
|
|
209
|
+
prompt_chunk = f"=== REPOSITORY: {repo_name} ===\n\n"
|
|
210
|
+
for file_info in files:
|
|
211
|
+
prompt_chunk += f"--- {file_info.path} ---\n"
|
|
212
|
+
try:
|
|
213
|
+
with open(file_info.path, "r", encoding="utf-8", errors="ignore") as src:
|
|
214
|
+
prompt_chunk += src.read() + "\n\n"
|
|
215
|
+
except Exception:
|
|
216
|
+
prompt_chunk += "[Error reading file contents]\n\n"
|
|
217
|
+
llm_report = prompt_chunk
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"idx": idx,
|
|
221
|
+
"repo_name": repo_name,
|
|
222
|
+
"exit_code": local_exit_code,
|
|
223
|
+
"badge_svg": badge_svg,
|
|
224
|
+
"badge_path": badge_path,
|
|
225
|
+
"terminal_report": terminal_report,
|
|
226
|
+
"json_report": json_report,
|
|
227
|
+
"html_report": html_report,
|
|
228
|
+
"llm_report": llm_report,
|
|
229
|
+
"duration": repo_duration,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
def main():
|
|
233
|
+
start_time = time.time()
|
|
234
|
+
args = parse_args()
|
|
235
|
+
|
|
236
|
+
# 1. Print Banner & Greeting
|
|
237
|
+
use_color = not args.no_color and sys.stdout.isatty()
|
|
238
|
+
def c(text, code):
|
|
239
|
+
return f"\033[{code}m{text}\033[0m" if use_color else text
|
|
240
|
+
|
|
241
|
+
if not args.json:
|
|
242
|
+
print()
|
|
243
|
+
print(c("╔════════════════════════════════════════════════════════════╗", "94;1"))
|
|
244
|
+
print(c("║ ", "94;1"), end="")
|
|
245
|
+
for char in "REPO DOCTOR":
|
|
246
|
+
print(c(char, "96;1"), end="")
|
|
247
|
+
sys.stdout.flush()
|
|
248
|
+
time.sleep(0.05)
|
|
249
|
+
print(c(" ║", "94;1"))
|
|
250
|
+
print(c("╚════════════════════════════════════════════════════════════╝", "94;1"))
|
|
251
|
+
print(c("Welcome to RepoDoctor! 🩺", "92;1"))
|
|
252
|
+
print(c("Initializing zero-dependency static analysis engine...", "90;1"))
|
|
253
|
+
print()
|
|
254
|
+
time.sleep(0.5)
|
|
255
|
+
|
|
256
|
+
root_paths = args.path
|
|
257
|
+
if not root_paths:
|
|
258
|
+
root_paths = ['.']
|
|
259
|
+
|
|
260
|
+
# Validate all directories first
|
|
261
|
+
for rp in root_paths:
|
|
262
|
+
if not os.path.isdir(rp):
|
|
263
|
+
print(f"Error: {rp} is not a directory.")
|
|
264
|
+
sys.exit(2)
|
|
265
|
+
|
|
266
|
+
custom_ignores = args.ignore.split(",") if args.ignore else []
|
|
267
|
+
show_animation = not getattr(args, "no_animation", False)
|
|
268
|
+
use_parallel = getattr(args, "parallel", False)
|
|
269
|
+
|
|
270
|
+
html_outputs = []
|
|
271
|
+
json_outputs = []
|
|
272
|
+
llm_outputs = []
|
|
273
|
+
exit_code = 0
|
|
274
|
+
|
|
275
|
+
# If scanning multiple repositories, notify the user we are processing them in parallel
|
|
276
|
+
if len(root_paths) > 1 and not args.json:
|
|
277
|
+
print(c(f"Starting parallel analysis on {len(root_paths)} repositories...", "96"))
|
|
278
|
+
print()
|
|
279
|
+
|
|
280
|
+
# Use ThreadPoolExecutor to run analyses in parallel
|
|
281
|
+
analysis_start_time = time.time()
|
|
282
|
+
max_workers = min(len(root_paths), (os.cpu_count() or 1) + 4)
|
|
283
|
+
results = []
|
|
284
|
+
|
|
285
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
286
|
+
futures = {
|
|
287
|
+
executor.submit(
|
|
288
|
+
process_single_repo, rp, args, idx, custom_ignores, use_parallel, show_animation, start_time
|
|
289
|
+
): rp
|
|
290
|
+
for idx, rp in enumerate(root_paths)
|
|
291
|
+
}
|
|
292
|
+
for future in concurrent.futures.as_completed(futures):
|
|
293
|
+
rp = futures[future]
|
|
294
|
+
try:
|
|
295
|
+
res = future.result()
|
|
296
|
+
results.append(res)
|
|
297
|
+
if len(root_paths) > 1 and not args.json:
|
|
298
|
+
print(c(f"✔ Completed analysis of {res['repo_name']}", "92"))
|
|
299
|
+
except Exception as e:
|
|
300
|
+
print(f"Error analyzing {rp}: {e}", file=sys.stderr)
|
|
301
|
+
exit_code = max(exit_code, 1)
|
|
302
|
+
|
|
303
|
+
if len(root_paths) > 1 and not args.json:
|
|
304
|
+
print()
|
|
305
|
+
print(c("All analyses completed. Generating reports...", "90"))
|
|
306
|
+
print()
|
|
307
|
+
|
|
308
|
+
# Sort results by their original path order to keep output deterministic
|
|
309
|
+
results.sort(key=lambda x: x["idx"])
|
|
310
|
+
|
|
311
|
+
for res in results:
|
|
312
|
+
# Update exit code
|
|
313
|
+
exit_code = max(exit_code, res["exit_code"])
|
|
314
|
+
|
|
315
|
+
# Write Badge
|
|
316
|
+
if res["badge_path"] and res["badge_svg"]:
|
|
317
|
+
try:
|
|
318
|
+
with open(res["badge_path"], "w", encoding="utf-8") as bf:
|
|
319
|
+
bf.write(res["badge_svg"])
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
# Print Terminal Report
|
|
324
|
+
if not args.json and res["terminal_report"]:
|
|
325
|
+
print(res["terminal_report"])
|
|
326
|
+
|
|
327
|
+
# Accumulate reports
|
|
328
|
+
if res["json_report"] is not None:
|
|
329
|
+
json_outputs.append(res["json_report"])
|
|
330
|
+
if res["html_report"] is not None:
|
|
331
|
+
html_outputs.append(res["html_report"])
|
|
332
|
+
if res["llm_report"] is not None:
|
|
333
|
+
llm_outputs.append(res["llm_report"])
|
|
334
|
+
|
|
335
|
+
if args.json and json_outputs:
|
|
336
|
+
import json
|
|
337
|
+
if len(json_outputs) == 1:
|
|
338
|
+
print(json.dumps(json_outputs[0], indent=2))
|
|
339
|
+
else:
|
|
340
|
+
print(json.dumps(json_outputs, indent=2))
|
|
341
|
+
|
|
342
|
+
if args.html and html_outputs:
|
|
343
|
+
try:
|
|
344
|
+
with open(args.html, "w", encoding="utf-8") as f:
|
|
345
|
+
f.write("\n<hr>\n<br><br>\n".join(html_outputs))
|
|
346
|
+
print(f"HTML report successfully written to {args.html}")
|
|
347
|
+
except Exception as e:
|
|
348
|
+
print(f"Failed to write HTML report: {e}")
|
|
349
|
+
sys.exit(3)
|
|
350
|
+
|
|
351
|
+
if args.export_prompt and llm_outputs:
|
|
352
|
+
try:
|
|
353
|
+
with open(args.export_prompt, "w", encoding="utf-8") as f:
|
|
354
|
+
f.write("\n\n".join(llm_outputs))
|
|
355
|
+
print(f"LLM prompt successfully exported to {args.export_prompt}")
|
|
356
|
+
except Exception as e:
|
|
357
|
+
print(f"Failed to export LLM prompt: {e}")
|
|
358
|
+
sys.exit(3)
|
|
359
|
+
|
|
360
|
+
if len(root_paths) > 1 and not args.json:
|
|
361
|
+
total_analysis_time = time.time() - analysis_start_time
|
|
362
|
+
print(c("────────────────────────────────────────────────────────────", "90"))
|
|
363
|
+
print(c(f"⚡ Concurrently analyzed {len(root_paths)} repositories in {total_analysis_time:.2f}s", "96;1"))
|
|
364
|
+
print(c("────────────────────────────────────────────────────────────", "90"))
|
|
365
|
+
print()
|
|
366
|
+
|
|
367
|
+
sys.exit(exit_code)
|
|
368
|
+
|
|
369
|
+
if __name__ == "__main__":
|
|
370
|
+
main()
|