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.
Files changed (40) hide show
  1. repodoctor_cli-1.0.0/PKG-INFO +137 -0
  2. repodoctor_cli-1.0.0/README.md +122 -0
  3. repodoctor_cli-1.0.0/pyproject.toml +30 -0
  4. repodoctor_cli-1.0.0/repodoctor/__init__.py +1 -0
  5. repodoctor_cli-1.0.0/repodoctor/__main__.py +370 -0
  6. repodoctor_cli-1.0.0/repodoctor/baseline.py +42 -0
  7. repodoctor_cli-1.0.0/repodoctor/cli.py +47 -0
  8. repodoctor_cli-1.0.0/repodoctor/duplicates.py +69 -0
  9. repodoctor_cli-1.0.0/repodoctor/git.py +67 -0
  10. repodoctor_cli-1.0.0/repodoctor/languages.py +33 -0
  11. repodoctor_cli-1.0.0/repodoctor/linter.py +226 -0
  12. repodoctor_cli-1.0.0/repodoctor/metrics.py +73 -0
  13. repodoctor_cli-1.0.0/repodoctor/models.py +81 -0
  14. repodoctor_cli-1.0.0/repodoctor/report.py +361 -0
  15. repodoctor_cli-1.0.0/repodoctor/scanner.py +177 -0
  16. repodoctor_cli-1.0.0/repodoctor/scoring.py +49 -0
  17. repodoctor_cli-1.0.0/repodoctor/security.py +56 -0
  18. repodoctor_cli-1.0.0/repodoctor/spinner.py +174 -0
  19. repodoctor_cli-1.0.0/repodoctor/structure.py +40 -0
  20. repodoctor_cli-1.0.0/repodoctor/todos.py +32 -0
  21. repodoctor_cli-1.0.0/repodoctor_cli.egg-info/PKG-INFO +137 -0
  22. repodoctor_cli-1.0.0/repodoctor_cli.egg-info/SOURCES.txt +38 -0
  23. repodoctor_cli-1.0.0/repodoctor_cli.egg-info/dependency_links.txt +1 -0
  24. repodoctor_cli-1.0.0/repodoctor_cli.egg-info/entry_points.txt +2 -0
  25. repodoctor_cli-1.0.0/repodoctor_cli.egg-info/top_level.txt +1 -0
  26. repodoctor_cli-1.0.0/setup.cfg +4 -0
  27. repodoctor_cli-1.0.0/setup.py +35 -0
  28. repodoctor_cli-1.0.0/tests/test_baseline.py +60 -0
  29. repodoctor_cli-1.0.0/tests/test_cli.py +28 -0
  30. repodoctor_cli-1.0.0/tests/test_duplicates.py +38 -0
  31. repodoctor_cli-1.0.0/tests/test_git.py +42 -0
  32. repodoctor_cli-1.0.0/tests/test_languages.py +14 -0
  33. repodoctor_cli-1.0.0/tests/test_metrics.py +32 -0
  34. repodoctor_cli-1.0.0/tests/test_packaging.py +108 -0
  35. repodoctor_cli-1.0.0/tests/test_parallel.py +93 -0
  36. repodoctor_cli-1.0.0/tests/test_scanner.py +44 -0
  37. repodoctor_cli-1.0.0/tests/test_scoring.py +31 -0
  38. repodoctor_cli-1.0.0/tests/test_security.py +37 -0
  39. repodoctor_cli-1.0.0/tests/test_structure.py +38 -0
  40. 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()