devdoctor-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.
- devdoctor/__init__.py +40 -0
- devdoctor/__main__.py +7 -0
- devdoctor/api.py +96 -0
- devdoctor/checks/__init__.py +38 -0
- devdoctor/checks/base.py +69 -0
- devdoctor/checks/dependencies.py +125 -0
- devdoctor/checks/django.py +128 -0
- devdoctor/checks/docker.py +124 -0
- devdoctor/checks/environment.py +146 -0
- devdoctor/checks/fastapi.py +96 -0
- devdoctor/checks/filesystem.py +72 -0
- devdoctor/checks/git.py +151 -0
- devdoctor/checks/imports.py +47 -0
- devdoctor/checks/pip.py +67 -0
- devdoctor/checks/ports.py +90 -0
- devdoctor/checks/postgres.py +65 -0
- devdoctor/checks/python.py +105 -0
- devdoctor/checks/redis.py +70 -0
- devdoctor/checks/virtualenv.py +79 -0
- devdoctor/cli.py +319 -0
- devdoctor/fixer.py +160 -0
- devdoctor/models.py +257 -0
- devdoctor/plugin.py +89 -0
- devdoctor/project.py +223 -0
- devdoctor/reporter.py +193 -0
- devdoctor/scanner.py +98 -0
- devdoctor/utils/__init__.py +1 -0
- devdoctor/utils/ast_imports.py +205 -0
- devdoctor/utils/env_parser.py +57 -0
- devdoctor/utils/platform_info.py +70 -0
- devdoctor/utils/process_utils.py +167 -0
- devdoctor_cli-0.1.0.dist-info/METADATA +382 -0
- devdoctor_cli-0.1.0.dist-info/RECORD +37 -0
- devdoctor_cli-0.1.0.dist-info/WHEEL +5 -0
- devdoctor_cli-0.1.0.dist-info/entry_points.txt +2 -0
- devdoctor_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- devdoctor_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Check environment variables, .env file presence, and .env vs .env.example discrepancies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
from devdoctor.checks.base import Check, register_check
|
|
11
|
+
from devdoctor.models import Confidence, Diagnostic, FixAction, ScanContext, Severity
|
|
12
|
+
from devdoctor.utils.ast_imports import scan_project_ast
|
|
13
|
+
from devdoctor.utils.env_parser import find_missing_env_keys, parse_env_file
|
|
14
|
+
from devdoctor.utils.platform_info import is_windows
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@register_check
|
|
18
|
+
class EnvironmentCheck(Check):
|
|
19
|
+
name = "environment"
|
|
20
|
+
title = "Environment Configuration"
|
|
21
|
+
category = "environment"
|
|
22
|
+
description = "Checks for missing environment files, missing keys from .env.example, and missing required vars found in code."
|
|
23
|
+
|
|
24
|
+
def run(self, context: ScanContext) -> List[Diagnostic]:
|
|
25
|
+
diagnostics: List[Diagnostic] = []
|
|
26
|
+
project_path = context.project_path
|
|
27
|
+
|
|
28
|
+
env_file = project_path / ".env"
|
|
29
|
+
env_example = project_path / ".env.example"
|
|
30
|
+
|
|
31
|
+
# Read active environment variables (system + .env if present)
|
|
32
|
+
loaded_env = dict(os.environ)
|
|
33
|
+
if env_file.is_file():
|
|
34
|
+
loaded_env.update(parse_env_file(env_file))
|
|
35
|
+
context.env_vars = loaded_env
|
|
36
|
+
|
|
37
|
+
# 1. Check .env vs .env.example
|
|
38
|
+
if env_example.is_file() and not env_file.is_file():
|
|
39
|
+
cp_cmd = "copy .env.example .env" if is_windows() else "cp .env.example .env"
|
|
40
|
+
|
|
41
|
+
def copy_example_handler(ctx: ScanContext) -> bool:
|
|
42
|
+
try:
|
|
43
|
+
shutil.copy2(env_example, env_file)
|
|
44
|
+
return True
|
|
45
|
+
except Exception:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
diagnostics.append(Diagnostic(
|
|
49
|
+
title="Environment file missing",
|
|
50
|
+
severity=Severity.ERROR,
|
|
51
|
+
message=".env file is missing, but .env.example exists.",
|
|
52
|
+
why="The project provides a template configuration (.env.example), but no active .env file was found.",
|
|
53
|
+
evidence="Found .env.example, but .env was not found.",
|
|
54
|
+
fix="Create .env from template:\n " + cp_cmd,
|
|
55
|
+
command=cp_cmd,
|
|
56
|
+
confidence=Confidence.HIGH,
|
|
57
|
+
fix_action=FixAction(
|
|
58
|
+
description="Copy .env.example to .env",
|
|
59
|
+
command=cp_cmd,
|
|
60
|
+
safe=True,
|
|
61
|
+
handler=copy_example_handler,
|
|
62
|
+
),
|
|
63
|
+
check_id=f"{self.name}_missing_env_file",
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
elif env_example.is_file() and env_file.is_file():
|
|
67
|
+
missing_from_env = find_missing_env_keys(env_example, env_file)
|
|
68
|
+
for key in missing_from_env:
|
|
69
|
+
def make_add_key_handler(k: str):
|
|
70
|
+
def handler(ctx: ScanContext) -> bool:
|
|
71
|
+
try:
|
|
72
|
+
with open(env_file, "a", encoding="utf-8") as f:
|
|
73
|
+
f.write(f"\n{k}=\n")
|
|
74
|
+
return True
|
|
75
|
+
except Exception:
|
|
76
|
+
return False
|
|
77
|
+
return handler
|
|
78
|
+
|
|
79
|
+
diagnostics.append(Diagnostic(
|
|
80
|
+
title="Environment variable",
|
|
81
|
+
severity=Severity.ERROR,
|
|
82
|
+
message=f"{key} is required by .env.example but missing in .env.",
|
|
83
|
+
why=f"{key} is defined in .env.example, indicating it is required for project runtime.",
|
|
84
|
+
evidence="Defined in: .env.example",
|
|
85
|
+
fix=f"Add {key} to .env",
|
|
86
|
+
command=f'echo {key}= >> .env',
|
|
87
|
+
confidence=Confidence.HIGH,
|
|
88
|
+
fix_action=FixAction(
|
|
89
|
+
description=f"Add {key} placeholder to .env",
|
|
90
|
+
command=f'echo {key}= >> .env',
|
|
91
|
+
safe=True,
|
|
92
|
+
handler=make_add_key_handler(key),
|
|
93
|
+
),
|
|
94
|
+
check_id=f"{self.name}_example_{key}",
|
|
95
|
+
metadata={"var_name": key, "source": ".env.example"},
|
|
96
|
+
))
|
|
97
|
+
|
|
98
|
+
# 2. Check code files for required environment variable accesses (AST)
|
|
99
|
+
_, env_usages = scan_project_ast(project_path)
|
|
100
|
+
reported_vars = set()
|
|
101
|
+
|
|
102
|
+
for usage in env_usages:
|
|
103
|
+
var_name = usage.var_name
|
|
104
|
+
# Only strictly required environment variable accesses without defaults
|
|
105
|
+
if not usage.is_required or usage.has_default or var_name in reported_vars:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Check if set in loaded_env
|
|
109
|
+
if var_name not in loaded_env or not loaded_env[var_name].strip():
|
|
110
|
+
reported_vars.add(var_name)
|
|
111
|
+
try:
|
|
112
|
+
rel_file = usage.file_path.relative_to(project_path)
|
|
113
|
+
except Exception:
|
|
114
|
+
rel_file = usage.file_path.name
|
|
115
|
+
|
|
116
|
+
def make_append_var_handler(v: str):
|
|
117
|
+
def handler(ctx: ScanContext) -> bool:
|
|
118
|
+
try:
|
|
119
|
+
target = ctx.project_path / ".env"
|
|
120
|
+
with open(target, "a", encoding="utf-8") as f:
|
|
121
|
+
f.write(f"\n{v}=\n")
|
|
122
|
+
return True
|
|
123
|
+
except Exception:
|
|
124
|
+
return False
|
|
125
|
+
return handler
|
|
126
|
+
|
|
127
|
+
diagnostics.append(Diagnostic(
|
|
128
|
+
title="Environment variable",
|
|
129
|
+
severity=Severity.ERROR,
|
|
130
|
+
message=f"{var_name} is required but missing.",
|
|
131
|
+
why=f"Code in {rel_file} accesses os.environ['{var_name}'] without a default value.",
|
|
132
|
+
evidence=f"Found in:\n {rel_file} (line {usage.line_number})",
|
|
133
|
+
fix=f"Add {var_name} to .env",
|
|
134
|
+
command=f'echo {var_name}= >> .env',
|
|
135
|
+
confidence=Confidence.HIGH,
|
|
136
|
+
fix_action=FixAction(
|
|
137
|
+
description=f"Add {var_name} to .env",
|
|
138
|
+
command=f'echo {var_name}= >> .env',
|
|
139
|
+
safe=True,
|
|
140
|
+
handler=make_append_var_handler(var_name),
|
|
141
|
+
),
|
|
142
|
+
check_id=f"{self.name}_code_{var_name}",
|
|
143
|
+
metadata={"var_name": var_name, "found_in": str(rel_file), "line": usage.line_number},
|
|
144
|
+
))
|
|
145
|
+
|
|
146
|
+
return diagnostics
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""FastAPI framework diagnostic checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import re
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from devdoctor.checks.base import Check, register_check
|
|
10
|
+
from devdoctor.models import Confidence, Diagnostic, FixAction, ScanContext, Severity
|
|
11
|
+
from devdoctor.utils.ast_imports import collect_python_files
|
|
12
|
+
from devdoctor.utils.process_utils import is_port_in_use
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register_check
|
|
16
|
+
class FastAPICheck(Check):
|
|
17
|
+
name = "fastapi"
|
|
18
|
+
title = "FastAPI Framework Diagnostics"
|
|
19
|
+
category = "framework"
|
|
20
|
+
description = "Checks FastAPI setup: Uvicorn availability, default port 8000, and CORS configuration."
|
|
21
|
+
|
|
22
|
+
def run(self, context: ScanContext) -> List[Diagnostic]:
|
|
23
|
+
diagnostics: List[Diagnostic] = []
|
|
24
|
+
is_fastapi = context.project_info and "FastAPI" in context.project_info.frameworks
|
|
25
|
+
|
|
26
|
+
if not is_fastapi:
|
|
27
|
+
return diagnostics
|
|
28
|
+
|
|
29
|
+
# 1. Uvicorn installed check
|
|
30
|
+
uvicorn_spec = importlib.util.find_spec("uvicorn")
|
|
31
|
+
if uvicorn_spec:
|
|
32
|
+
diagnostics.append(Diagnostic(
|
|
33
|
+
title="Uvicorn installed",
|
|
34
|
+
severity=Severity.OK,
|
|
35
|
+
message="Uvicorn ASGI server is installed.",
|
|
36
|
+
check_id=f"{self.name}_uvicorn_installed",
|
|
37
|
+
))
|
|
38
|
+
else:
|
|
39
|
+
diagnostics.append(Diagnostic(
|
|
40
|
+
title="Uvicorn not installed",
|
|
41
|
+
severity=Severity.ERROR,
|
|
42
|
+
message="Uvicorn is required to serve FastAPI applications.",
|
|
43
|
+
why="FastAPI applications need an ASGI server like Uvicorn to run.",
|
|
44
|
+
fix="Install uvicorn:\n pip install 'uvicorn[standard]'",
|
|
45
|
+
command="pip install 'uvicorn[standard]'",
|
|
46
|
+
confidence=Confidence.HIGH,
|
|
47
|
+
fix_action=FixAction(
|
|
48
|
+
description="Install uvicorn[standard]",
|
|
49
|
+
command="pip install 'uvicorn[standard]'",
|
|
50
|
+
safe=False,
|
|
51
|
+
),
|
|
52
|
+
check_id=f"{self.name}_uvicorn_missing",
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
# 2. Port 8000 check
|
|
56
|
+
if is_port_in_use(8000):
|
|
57
|
+
diagnostics.append(Diagnostic(
|
|
58
|
+
title="Port 8000 occupied",
|
|
59
|
+
severity=Severity.WARNING,
|
|
60
|
+
message="Default FastAPI port (8000) is currently occupied.",
|
|
61
|
+
why="Uvicorn defaults to port 8000; starting a new server without --port will fail.",
|
|
62
|
+
fix="Free port 8000 or specify another port:\n devdoctor port free 8000",
|
|
63
|
+
command="devdoctor port free 8000",
|
|
64
|
+
confidence=Confidence.HIGH,
|
|
65
|
+
check_id=f"{self.name}_port_8000",
|
|
66
|
+
))
|
|
67
|
+
else:
|
|
68
|
+
diagnostics.append(Diagnostic(
|
|
69
|
+
title="Port 8000 available",
|
|
70
|
+
severity=Severity.OK,
|
|
71
|
+
message="Port 8000 is free and ready for Uvicorn.",
|
|
72
|
+
check_id=f"{self.name}_port_8000_free",
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
# 3. CORS allows * check
|
|
76
|
+
py_files = collect_python_files(context.project_path)
|
|
77
|
+
for pf in py_files:
|
|
78
|
+
try:
|
|
79
|
+
txt = pf.read_text(encoding="utf-8", errors="replace")
|
|
80
|
+
if 'allow_origins=["*"]' in txt or "allow_origins=['*']" in txt:
|
|
81
|
+
rel_p = pf.relative_to(context.project_path) if pf.is_relative_to(context.project_path) else pf.name
|
|
82
|
+
diagnostics.append(Diagnostic(
|
|
83
|
+
title="CORS allows * wildcard",
|
|
84
|
+
severity=Severity.WARNING,
|
|
85
|
+
message="CORS middleware is configured with allow_origins=['*'].",
|
|
86
|
+
why="Allowing wildcard origins with credentials or in production allows any domain to send authenticated cross-origin requests.",
|
|
87
|
+
evidence=f"Found in:\n {rel_p}",
|
|
88
|
+
fix="Restrict allow_origins to specific frontend domains.",
|
|
89
|
+
confidence=Confidence.HIGH,
|
|
90
|
+
check_id=f"{self.name}_cors_wildcard",
|
|
91
|
+
))
|
|
92
|
+
break
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
return diagnostics
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Check filesystem permissions, disk space, and repository clutter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
from devdoctor.checks.base import Check, register_check
|
|
11
|
+
from devdoctor.models import Confidence, Diagnostic, FixAction, ScanContext, Severity
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@register_check
|
|
15
|
+
class FilesystemCheck(Check):
|
|
16
|
+
name = "filesystem"
|
|
17
|
+
title = "Filesystem Integrity"
|
|
18
|
+
category = "filesystem"
|
|
19
|
+
description = "Checks filesystem permissions, write access, and unwanted repository clutter."
|
|
20
|
+
|
|
21
|
+
def run(self, context: ScanContext) -> List[Diagnostic]:
|
|
22
|
+
diagnostics: List[Diagnostic] = []
|
|
23
|
+
root = context.project_path
|
|
24
|
+
|
|
25
|
+
# 1. Test write permissions in project directory
|
|
26
|
+
try:
|
|
27
|
+
test_file = root / f".devdoctor_test_{os.getpid()}.tmp"
|
|
28
|
+
test_file.write_text("ok", encoding="utf-8")
|
|
29
|
+
test_file.unlink()
|
|
30
|
+
except Exception as ex:
|
|
31
|
+
diagnostics.append(Diagnostic(
|
|
32
|
+
title="Filesystem write permission denied",
|
|
33
|
+
severity=Severity.ERROR,
|
|
34
|
+
message=f"Project directory is not writable: {ex}",
|
|
35
|
+
why="DevDoctor and build tools require write access to create cache files, virtualenvs, and configs.",
|
|
36
|
+
evidence=f"Directory: {root}",
|
|
37
|
+
fix="Adjust directory permissions to grant write access to the current user.",
|
|
38
|
+
confidence=Confidence.HIGH,
|
|
39
|
+
check_id=f"{self.name}_write_permission",
|
|
40
|
+
))
|
|
41
|
+
|
|
42
|
+
# 2. Check for large uncommitted files (> 50MB) in project (ignoring .git and .venv)
|
|
43
|
+
large_files = []
|
|
44
|
+
ignore_dirs = {".git", ".venv", "venv", "node_modules", "dist", "build"}
|
|
45
|
+
try:
|
|
46
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
47
|
+
dirnames[:] = [d for d in dirnames if d not in ignore_dirs]
|
|
48
|
+
for f in filenames:
|
|
49
|
+
fp = Path(dirpath) / f
|
|
50
|
+
try:
|
|
51
|
+
sz = fp.stat().st_size
|
|
52
|
+
if sz > 50 * 1024 * 1024: # 50MB
|
|
53
|
+
large_files.append((fp, sz))
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
if large_files:
|
|
60
|
+
file_list = "\n ".join([f"{fp.relative_to(root)} ({sz // (1024*1024)}MB)" for fp, sz in large_files[:3]])
|
|
61
|
+
diagnostics.append(Diagnostic(
|
|
62
|
+
title="Large files detected in repository",
|
|
63
|
+
severity=Severity.WARNING,
|
|
64
|
+
message=f"Found {len(large_files)} file(s) exceeding 50MB.",
|
|
65
|
+
why="Large files bloat git repository size and can cause git pushes to fail or slow down operations.",
|
|
66
|
+
evidence=f"Found:\n {file_list}",
|
|
67
|
+
fix="Track large files using Git LFS or add them to .gitignore.",
|
|
68
|
+
confidence=Confidence.HIGH,
|
|
69
|
+
check_id=f"{self.name}_large_files",
|
|
70
|
+
))
|
|
71
|
+
|
|
72
|
+
return diagnostics
|
devdoctor/checks/git.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Check Git availability, author configuration, and sensitive file tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
from devdoctor.checks.base import Check, register_check
|
|
11
|
+
from devdoctor.models import Confidence, Diagnostic, FixAction, ScanContext, Severity
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@register_check
|
|
15
|
+
class GitCheck(Check):
|
|
16
|
+
name = "git"
|
|
17
|
+
title = "Git Configuration"
|
|
18
|
+
category = "git"
|
|
19
|
+
description = "Verifies Git CLI availability, repository status, and security of sensitive files."
|
|
20
|
+
|
|
21
|
+
def run(self, context: ScanContext) -> List[Diagnostic]:
|
|
22
|
+
diagnostics: List[Diagnostic] = []
|
|
23
|
+
|
|
24
|
+
# 1. Check Git CLI availability
|
|
25
|
+
git_version = None
|
|
26
|
+
try:
|
|
27
|
+
res = subprocess.run(["git", "--version"], capture_output=True, text=True, timeout=5)
|
|
28
|
+
if res.returncode == 0:
|
|
29
|
+
out = res.stdout.strip()
|
|
30
|
+
match = re.search(r"git version\s+([0-9]+(?:\.[0-9]+)+)", out)
|
|
31
|
+
git_version = match.group(1).rstrip(".") if match else "available"
|
|
32
|
+
diagnostics.append(Diagnostic(
|
|
33
|
+
title=f"Git {git_version}",
|
|
34
|
+
severity=Severity.OK,
|
|
35
|
+
message=f"Git is installed and available (version {git_version}).",
|
|
36
|
+
check_id=f"{self.name}_version",
|
|
37
|
+
metadata={"version": git_version},
|
|
38
|
+
))
|
|
39
|
+
else:
|
|
40
|
+
diagnostics.append(Diagnostic(
|
|
41
|
+
title="Git CLI unavailable",
|
|
42
|
+
severity=Severity.WARNING,
|
|
43
|
+
message="Git command failed or is not installed.",
|
|
44
|
+
why="Git is required for version control, dependency tracking, and release management.",
|
|
45
|
+
fix="Install Git from https://git-scm.com/downloads.",
|
|
46
|
+
confidence=Confidence.HIGH,
|
|
47
|
+
check_id=f"{self.name}_missing",
|
|
48
|
+
))
|
|
49
|
+
return diagnostics
|
|
50
|
+
except Exception:
|
|
51
|
+
diagnostics.append(Diagnostic(
|
|
52
|
+
title="Git CLI unavailable",
|
|
53
|
+
severity=Severity.WARNING,
|
|
54
|
+
message="Git is not installed or not in PATH.",
|
|
55
|
+
why="Version control commands and repository checks require git.",
|
|
56
|
+
fix="Install Git and ensure it is added to your PATH.",
|
|
57
|
+
confidence=Confidence.HIGH,
|
|
58
|
+
check_id=f"{self.name}_missing",
|
|
59
|
+
))
|
|
60
|
+
return diagnostics
|
|
61
|
+
|
|
62
|
+
# 2. Check if this is a Git repository
|
|
63
|
+
git_dir = context.project_path / ".git"
|
|
64
|
+
if not git_dir.exists():
|
|
65
|
+
return diagnostics
|
|
66
|
+
|
|
67
|
+
# Check git config user.name and user.email
|
|
68
|
+
try:
|
|
69
|
+
res_name = subprocess.run(["git", "config", "user.name"], capture_output=True, text=True, cwd=str(context.project_path))
|
|
70
|
+
res_email = subprocess.run(["git", "config", "user.email"], capture_output=True, text=True, cwd=str(context.project_path))
|
|
71
|
+
has_name = bool(res_name.stdout.strip())
|
|
72
|
+
has_email = bool(res_email.stdout.strip())
|
|
73
|
+
|
|
74
|
+
if not (has_name and has_email):
|
|
75
|
+
diagnostics.append(Diagnostic(
|
|
76
|
+
title="Git author configuration",
|
|
77
|
+
severity=Severity.WARNING,
|
|
78
|
+
message="Git user.name or user.email is not configured.",
|
|
79
|
+
why="Commits cannot be properly authored without user name and email configured.",
|
|
80
|
+
evidence=f"user.name='{res_name.stdout.strip()}', user.email='{res_email.stdout.strip()}'",
|
|
81
|
+
fix="Set your Git author details:\n git config --global user.name \"Your Name\"\n git config --global user.email \"you@example.com\"",
|
|
82
|
+
command='git config --global user.name "Your Name" && git config --global user.email "you@example.com"',
|
|
83
|
+
confidence=Confidence.HIGH,
|
|
84
|
+
check_id=f"{self.name}_author",
|
|
85
|
+
))
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
# 3. Check .gitignore for .env
|
|
90
|
+
env_file = context.project_path / ".env"
|
|
91
|
+
gitignore_file = context.project_path / ".gitignore"
|
|
92
|
+
|
|
93
|
+
if env_file.is_file():
|
|
94
|
+
# Check if .env is ignored
|
|
95
|
+
is_ignored = False
|
|
96
|
+
if gitignore_file.is_file():
|
|
97
|
+
try:
|
|
98
|
+
lines = [line.strip() for line in gitignore_file.read_text(encoding="utf-8", errors="replace").splitlines()]
|
|
99
|
+
is_ignored = any(line in [".env", "*.env", ".env*", "/.env"] for line in lines)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
if not is_ignored:
|
|
104
|
+
diagnostics.append(Diagnostic(
|
|
105
|
+
title="Sensitive file .env not ignored",
|
|
106
|
+
severity=Severity.ERROR,
|
|
107
|
+
message=".env file exists but is not listed in .gitignore, risking secret leaks.",
|
|
108
|
+
why="Committing .env files exposes sensitive credentials, secret keys, and database passwords in git history.",
|
|
109
|
+
evidence="Found .env in project root, but '.env' pattern was not found in .gitignore.",
|
|
110
|
+
fix="Add .env to .gitignore",
|
|
111
|
+
command="echo .env >> .gitignore",
|
|
112
|
+
confidence=Confidence.HIGH,
|
|
113
|
+
fix_action=FixAction(
|
|
114
|
+
description="Add .env to .gitignore",
|
|
115
|
+
safe=True,
|
|
116
|
+
action_type="file_append",
|
|
117
|
+
target_path=gitignore_file,
|
|
118
|
+
content=".env",
|
|
119
|
+
),
|
|
120
|
+
check_id=f"{self.name}_gitignore_env",
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
# Check if .env is already tracked in git index
|
|
124
|
+
try:
|
|
125
|
+
res_tracked = subprocess.run(
|
|
126
|
+
["git", "ls-files", "--error-unmatch", ".env"],
|
|
127
|
+
capture_output=True,
|
|
128
|
+
text=True,
|
|
129
|
+
cwd=str(context.project_path),
|
|
130
|
+
)
|
|
131
|
+
if res_tracked.returncode == 0:
|
|
132
|
+
diagnostics.append(Diagnostic(
|
|
133
|
+
title=".env is tracked by Git",
|
|
134
|
+
severity=Severity.ERROR,
|
|
135
|
+
message=".env file is tracked in git index! Secrets may already be committed.",
|
|
136
|
+
why="A tracked .env will be pushed to remotes, compromising credentials.",
|
|
137
|
+
evidence="git ls-files .env confirmed tracking.",
|
|
138
|
+
fix="Remove .env from git cache:\n git rm --cached .env",
|
|
139
|
+
command="git rm --cached .env",
|
|
140
|
+
confidence=Confidence.HIGH,
|
|
141
|
+
fix_action=FixAction(
|
|
142
|
+
description="Remove .env from git cache",
|
|
143
|
+
command="git rm --cached .env",
|
|
144
|
+
safe=False,
|
|
145
|
+
),
|
|
146
|
+
check_id=f"{self.name}_tracked_env",
|
|
147
|
+
))
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
return diagnostics
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Check for missing or uninstalled Python imports in project code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from devdoctor.checks.base import Check, register_check
|
|
8
|
+
from devdoctor.models import Confidence, Diagnostic, FixAction, ScanContext, Severity
|
|
9
|
+
from devdoctor.utils.ast_imports import find_missing_installed_imports
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@register_check
|
|
13
|
+
class BrokenImportsCheck(Check):
|
|
14
|
+
name = "broken_imports"
|
|
15
|
+
title = "Python Imports"
|
|
16
|
+
category = "dependencies"
|
|
17
|
+
description = "Scans project Python files for third-party imports that are not installed in the active environment."
|
|
18
|
+
|
|
19
|
+
def run(self, context: ScanContext) -> List[Diagnostic]:
|
|
20
|
+
diagnostics: List[Diagnostic] = []
|
|
21
|
+
missing = find_missing_installed_imports(context.project_path)
|
|
22
|
+
|
|
23
|
+
for mod, file_path, line_no in missing:
|
|
24
|
+
try:
|
|
25
|
+
rel_path = file_path.relative_to(context.project_path)
|
|
26
|
+
except Exception:
|
|
27
|
+
rel_path = file_path.name
|
|
28
|
+
|
|
29
|
+
diagnostics.append(Diagnostic(
|
|
30
|
+
title=f"Missing module '{mod}'",
|
|
31
|
+
severity=Severity.ERROR,
|
|
32
|
+
message=f"Module '{mod}' is imported but not installed in current environment.",
|
|
33
|
+
why=f"Code in {rel_path} requires '{mod}', but the package is not found in site-packages.",
|
|
34
|
+
evidence=f"Found in:\n {rel_path} (line {line_no})",
|
|
35
|
+
fix=f"Install {mod} via pip:\n pip install {mod}",
|
|
36
|
+
command=f"pip install {mod}",
|
|
37
|
+
confidence=Confidence.HIGH,
|
|
38
|
+
fix_action=FixAction(
|
|
39
|
+
description=f"Install {mod}",
|
|
40
|
+
command=f"pip install {mod}",
|
|
41
|
+
safe=False,
|
|
42
|
+
),
|
|
43
|
+
check_id=f"{self.name}_{mod}",
|
|
44
|
+
metadata={"module": mod, "file": str(rel_path), "line": line_no},
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
return diagnostics
|
devdoctor/checks/pip.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Check pip installation and availability."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from devdoctor.checks.base import Check, register_check
|
|
10
|
+
from devdoctor.models import Confidence, Diagnostic, FixAction, ScanContext, Severity
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@register_check
|
|
14
|
+
class PipAvailabilityCheck(Check):
|
|
15
|
+
name = "pip_availability"
|
|
16
|
+
title = "pip Availability"
|
|
17
|
+
category = "system"
|
|
18
|
+
description = "Checks that pip is available and functional in the active environment."
|
|
19
|
+
|
|
20
|
+
def run(self, context: ScanContext) -> Diagnostic:
|
|
21
|
+
try:
|
|
22
|
+
res = subprocess.run(
|
|
23
|
+
[sys.executable, "-m", "pip", "--version"],
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
timeout=5,
|
|
27
|
+
)
|
|
28
|
+
if res.returncode == 0:
|
|
29
|
+
output = res.stdout.strip()
|
|
30
|
+
# e.g., "pip 25.0.1 from ..."
|
|
31
|
+
match = re.search(r"pip\s+([0-9.]+)", output)
|
|
32
|
+
version_str = match.group(1) if match else "available"
|
|
33
|
+
return Diagnostic(
|
|
34
|
+
title=f"pip {version_str}",
|
|
35
|
+
severity=Severity.OK,
|
|
36
|
+
message=f"pip is installed and functional (version {version_str}).",
|
|
37
|
+
check_id=self.name,
|
|
38
|
+
metadata={"version": version_str},
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
return Diagnostic(
|
|
42
|
+
title="pip unavailable",
|
|
43
|
+
severity=Severity.ERROR,
|
|
44
|
+
message="pip command failed to execute in active Python environment.",
|
|
45
|
+
why="pip might be corrupt or missing in this Python installation.",
|
|
46
|
+
evidence=res.stderr.strip() or res.stdout.strip(),
|
|
47
|
+
fix="Run ensurepip to bootstrap pip into this environment.",
|
|
48
|
+
command=f"{sys.executable} -m ensurepip --upgrade",
|
|
49
|
+
confidence=Confidence.HIGH,
|
|
50
|
+
fix_action=FixAction(
|
|
51
|
+
description="Bootstrap pip via ensurepip",
|
|
52
|
+
command=f"{sys.executable} -m ensurepip --upgrade",
|
|
53
|
+
safe=True,
|
|
54
|
+
),
|
|
55
|
+
check_id=self.name,
|
|
56
|
+
)
|
|
57
|
+
except Exception as ex:
|
|
58
|
+
return Diagnostic(
|
|
59
|
+
title="pip unavailable",
|
|
60
|
+
severity=Severity.ERROR,
|
|
61
|
+
message=f"Unable to execute pip: {ex}",
|
|
62
|
+
why="Python executable cannot find or execute the pip module.",
|
|
63
|
+
fix="Bootstrap pip or repair Python installation.",
|
|
64
|
+
command=f"{sys.executable} -m ensurepip --upgrade",
|
|
65
|
+
confidence=Confidence.HIGH,
|
|
66
|
+
check_id=self.name,
|
|
67
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Check port availability and detect process conflicts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import List, Set
|
|
7
|
+
|
|
8
|
+
from devdoctor.checks.base import Check, register_check
|
|
9
|
+
from devdoctor.models import Confidence, Diagnostic, FixAction, ScanContext, Severity
|
|
10
|
+
from devdoctor.utils.process_utils import get_port_listener_info, is_port_in_use
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@register_check
|
|
14
|
+
class PortConflictsCheck(Check):
|
|
15
|
+
name = "port_conflicts"
|
|
16
|
+
title = "Port Conflicts"
|
|
17
|
+
category = "network"
|
|
18
|
+
description = "Checks common development ports for process occupation and port collisions."
|
|
19
|
+
|
|
20
|
+
def run(self, context: ScanContext) -> List[Diagnostic]:
|
|
21
|
+
diagnostics: List[Diagnostic] = []
|
|
22
|
+
|
|
23
|
+
# Determine ports to check based on project and common dev ports
|
|
24
|
+
target_ports: Set[int] = set()
|
|
25
|
+
|
|
26
|
+
# Check project type defaults
|
|
27
|
+
proj = context.project_info
|
|
28
|
+
if proj:
|
|
29
|
+
if proj.project_type in ("Django", "FastAPI"):
|
|
30
|
+
target_ports.add(8000)
|
|
31
|
+
elif proj.project_type == "Flask":
|
|
32
|
+
target_ports.add(5000)
|
|
33
|
+
if "Node.js" in proj.technologies:
|
|
34
|
+
target_ports.update([3000, 5173])
|
|
35
|
+
|
|
36
|
+
# Check .env for PORT=...
|
|
37
|
+
if context.env_vars:
|
|
38
|
+
for k, v in context.env_vars.items():
|
|
39
|
+
if "PORT" in k:
|
|
40
|
+
try:
|
|
41
|
+
p = int(v.strip())
|
|
42
|
+
if 1 <= p <= 65535:
|
|
43
|
+
target_ports.add(p)
|
|
44
|
+
except ValueError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# If no specific ports found, check the standard dev ports
|
|
48
|
+
if not target_ports:
|
|
49
|
+
target_ports = {8000, 3000, 5000, 5173, 8080}
|
|
50
|
+
|
|
51
|
+
for port in sorted(list(target_ports)):
|
|
52
|
+
if is_port_in_use(port):
|
|
53
|
+
info = get_port_listener_info(port)
|
|
54
|
+
proc_name = info.process_name if info else "unknown"
|
|
55
|
+
pid = info.pid if info else 0
|
|
56
|
+
|
|
57
|
+
evidence_lines = []
|
|
58
|
+
if proc_name and proc_name != "unknown":
|
|
59
|
+
evidence_lines.append(f"Process: {proc_name}")
|
|
60
|
+
if pid > 0:
|
|
61
|
+
evidence_lines.append(f"PID: {pid}")
|
|
62
|
+
|
|
63
|
+
evidence_text = "\n ".join(evidence_lines) if evidence_lines else "Listening on local interface"
|
|
64
|
+
fix_cmd = f"devdoctor port free {port}"
|
|
65
|
+
|
|
66
|
+
fix_action = None
|
|
67
|
+
if pid > 0:
|
|
68
|
+
fix_action = FixAction(
|
|
69
|
+
description=f"Kill process {proc_name} (PID: {pid}) on port {port}",
|
|
70
|
+
command=fix_cmd,
|
|
71
|
+
safe=False,
|
|
72
|
+
action_type="kill_process",
|
|
73
|
+
metadata={"port": port, "pid": pid, "process_name": proc_name},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
diagnostics.append(Diagnostic(
|
|
77
|
+
title=f"Port {port}",
|
|
78
|
+
severity=Severity.WARNING,
|
|
79
|
+
message="Port is already occupied.",
|
|
80
|
+
why=f"Another process ({proc_name}) is already listening on port {port}, which prevents starting the local dev server.",
|
|
81
|
+
evidence=evidence_text,
|
|
82
|
+
fix=f"devdoctor port free {port}",
|
|
83
|
+
command=fix_cmd,
|
|
84
|
+
confidence=Confidence.HIGH,
|
|
85
|
+
fix_action=fix_action,
|
|
86
|
+
check_id=f"{self.name}_{port}",
|
|
87
|
+
metadata={"port": port, "pid": pid, "process_name": proc_name},
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
return diagnostics
|