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,65 @@
|
|
|
1
|
+
"""PostgreSQL service and connection diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import socket
|
|
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.platform_info import is_windows
|
|
12
|
+
from devdoctor.utils.process_utils import is_port_in_use
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register_check
|
|
16
|
+
class PostgresCheck(Check):
|
|
17
|
+
name = "postgres"
|
|
18
|
+
title = "PostgreSQL Diagnostics"
|
|
19
|
+
category = "framework"
|
|
20
|
+
description = "Checks PostgreSQL service availability, port 5432 reachability, and configuration."
|
|
21
|
+
|
|
22
|
+
def run(self, context: ScanContext) -> List[Diagnostic]:
|
|
23
|
+
diagnostics: List[Diagnostic] = []
|
|
24
|
+
is_postgres_project = context.project_info and "PostgreSQL" in context.project_info.technologies
|
|
25
|
+
|
|
26
|
+
# Check port 5432
|
|
27
|
+
port_open = is_port_in_use(5432)
|
|
28
|
+
|
|
29
|
+
if port_open:
|
|
30
|
+
diagnostics.append(Diagnostic(
|
|
31
|
+
title="PostgreSQL service listening",
|
|
32
|
+
severity=Severity.OK,
|
|
33
|
+
message="PostgreSQL server is listening on port 5432.",
|
|
34
|
+
check_id=f"{self.name}_port_ok",
|
|
35
|
+
))
|
|
36
|
+
elif is_postgres_project:
|
|
37
|
+
start_cmd = "docker compose up -d postgres" if (context.project_path / "docker-compose.yml").exists() else ("net start postgresql-x64" if is_windows() else "sudo systemctl start postgresql")
|
|
38
|
+
diagnostics.append(Diagnostic(
|
|
39
|
+
title="PostgreSQL not running",
|
|
40
|
+
severity=Severity.ERROR,
|
|
41
|
+
message="PostgreSQL service is not listening on default port 5432.",
|
|
42
|
+
why="The project requires PostgreSQL, but no service was detected on port 5432.",
|
|
43
|
+
evidence="Port 5432 connection refused.",
|
|
44
|
+
fix=f"Start PostgreSQL service:\n {start_cmd}",
|
|
45
|
+
command=start_cmd,
|
|
46
|
+
confidence=Confidence.HIGH,
|
|
47
|
+
check_id=f"{self.name}_not_running",
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
# Check DATABASE_URL format if present in env
|
|
51
|
+
db_url = context.env_vars.get("DATABASE_URL")
|
|
52
|
+
if db_url:
|
|
53
|
+
if not re.match(r"^postgres(ql)?:\/\/", db_url):
|
|
54
|
+
diagnostics.append(Diagnostic(
|
|
55
|
+
title="Malformed DATABASE_URL",
|
|
56
|
+
severity=Severity.WARNING,
|
|
57
|
+
message="DATABASE_URL does not start with postgres:// or postgresql://.",
|
|
58
|
+
why=f"DATABASE_URL value is '{db_url[:15]}...', which might not be a valid PostgreSQL connection string.",
|
|
59
|
+
evidence=f"DATABASE_URL = {db_url}",
|
|
60
|
+
fix="Verify and correct the connection URL in .env.",
|
|
61
|
+
confidence=Confidence.MEDIUM,
|
|
62
|
+
check_id=f"{self.name}_malformed_url",
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
return diagnostics
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Check Python runtime version and project compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
11
|
+
from packaging.version import Version
|
|
12
|
+
|
|
13
|
+
from devdoctor.checks.base import Check, register_check
|
|
14
|
+
from devdoctor.models import Confidence, Diagnostic, FixAction, ScanContext, Severity
|
|
15
|
+
from devdoctor.utils.platform_info import get_python_version_str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_required_python_spec(project_path: Path) -> Optional[str]:
|
|
19
|
+
"""Read Python version requirement from pyproject.toml, .python-version, or runtime.txt."""
|
|
20
|
+
pyproject = project_path / "pyproject.toml"
|
|
21
|
+
if pyproject.is_file():
|
|
22
|
+
try:
|
|
23
|
+
content = pyproject.read_text(encoding="utf-8", errors="replace")
|
|
24
|
+
# Match requires-python = ">=3.11"
|
|
25
|
+
match = re.search(r'requires-python\s*=\s*["\']([^"\']+)["\']', content)
|
|
26
|
+
if match:
|
|
27
|
+
return match.group(1).strip()
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
python_version_file = project_path / ".python-version"
|
|
32
|
+
if python_version_file.is_file():
|
|
33
|
+
try:
|
|
34
|
+
version_str = python_version_file.read_text(encoding="utf-8", errors="replace").strip()
|
|
35
|
+
if version_str:
|
|
36
|
+
return f"=={version_str}" if not any(c in version_str for c in "<>=~") else version_str
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
runtime_txt = project_path / "runtime.txt"
|
|
41
|
+
if runtime_txt.is_file():
|
|
42
|
+
try:
|
|
43
|
+
content = runtime_txt.read_text(encoding="utf-8", errors="replace").strip()
|
|
44
|
+
# python-3.12.2 -> ==3.12.2
|
|
45
|
+
match = re.search(r"python-([0-9.]+)", content)
|
|
46
|
+
if match:
|
|
47
|
+
return f"=={match.group(1)}"
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@register_check
|
|
55
|
+
class PythonVersionCheck(Check):
|
|
56
|
+
name = "python_version"
|
|
57
|
+
title = "Python Version"
|
|
58
|
+
category = "system"
|
|
59
|
+
description = "Verifies Python version and project requirement compatibility."
|
|
60
|
+
|
|
61
|
+
def run(self, context: ScanContext) -> Diagnostic:
|
|
62
|
+
current_version_str = get_python_version_str()
|
|
63
|
+
current_ver = Version(current_version_str)
|
|
64
|
+
spec_str = get_required_python_spec(context.project_path)
|
|
65
|
+
|
|
66
|
+
if spec_str:
|
|
67
|
+
try:
|
|
68
|
+
spec = SpecifierSet(spec_str)
|
|
69
|
+
if current_ver not in spec:
|
|
70
|
+
return Diagnostic(
|
|
71
|
+
title="Python version incompatible",
|
|
72
|
+
severity=Severity.ERROR,
|
|
73
|
+
message=f"Your project requires Python {spec_str}, but you are running {current_version_str}.",
|
|
74
|
+
why=f"The current Python runtime ({current_version_str}) does not satisfy the specified version constraints.",
|
|
75
|
+
evidence=f"Active: {current_version_str} | Required: {spec_str}",
|
|
76
|
+
fix=f"Install Python {spec_str} and recreate your virtual environment.",
|
|
77
|
+
command=f"py -{spec_str.replace('>=', '').replace('==', '')} -m venv .venv",
|
|
78
|
+
confidence=Confidence.HIGH,
|
|
79
|
+
check_id=self.name,
|
|
80
|
+
metadata={"current_version": current_version_str, "required_spec": spec_str},
|
|
81
|
+
)
|
|
82
|
+
except InvalidSpecifier:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Check for ancient / unsupported Python versions (< 3.9)
|
|
86
|
+
if sys.version_info < (3, 9):
|
|
87
|
+
return Diagnostic(
|
|
88
|
+
title=f"Python {current_version_str}",
|
|
89
|
+
severity=Severity.WARNING,
|
|
90
|
+
message=f"Python {current_version_str} is End-Of-Life and no longer receives security updates.",
|
|
91
|
+
why="Python versions below 3.9 are deprecated by python.org and most modern libraries.",
|
|
92
|
+
evidence=f"Active runtime version: {current_version_str}",
|
|
93
|
+
fix="Upgrade to Python 3.11 or 3.12 for improved performance and security.",
|
|
94
|
+
command="py -3.12 -m venv .venv",
|
|
95
|
+
confidence=Confidence.HIGH,
|
|
96
|
+
check_id=self.name,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return Diagnostic(
|
|
100
|
+
title=f"Python {current_version_str}",
|
|
101
|
+
severity=Severity.OK,
|
|
102
|
+
message=f"Python {current_version_str} is installed and compatible.",
|
|
103
|
+
check_id=self.name,
|
|
104
|
+
metadata={"version": current_version_str},
|
|
105
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Redis server service and ping diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import socket
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from devdoctor.checks.base import Check, register_check
|
|
9
|
+
from devdoctor.models import Confidence, Diagnostic, ScanContext, Severity
|
|
10
|
+
from devdoctor.utils.process_utils import is_port_in_use
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@register_check
|
|
14
|
+
class RedisCheck(Check):
|
|
15
|
+
name = "redis"
|
|
16
|
+
title = "Redis Diagnostics"
|
|
17
|
+
category = "framework"
|
|
18
|
+
description = "Checks Redis server availability on port 6379 and RESP PING responsiveness."
|
|
19
|
+
|
|
20
|
+
def run(self, context: ScanContext) -> List[Diagnostic]:
|
|
21
|
+
diagnostics: List[Diagnostic] = []
|
|
22
|
+
is_redis_project = context.project_info and "Redis" in context.project_info.technologies
|
|
23
|
+
|
|
24
|
+
# Check port 6379
|
|
25
|
+
port_open = is_port_in_use(6379)
|
|
26
|
+
|
|
27
|
+
if port_open:
|
|
28
|
+
# Try raw RESP PING
|
|
29
|
+
ping_ok = False
|
|
30
|
+
try:
|
|
31
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
32
|
+
s.settimeout(1.0)
|
|
33
|
+
s.connect(("127.0.0.1", 6379))
|
|
34
|
+
s.sendall(b"*1\r\n$4\r\nPING\r\n")
|
|
35
|
+
response = s.recv(1024)
|
|
36
|
+
if b"+PONG" in response:
|
|
37
|
+
ping_ok = True
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
if ping_ok:
|
|
42
|
+
diagnostics.append(Diagnostic(
|
|
43
|
+
title="Redis healthy",
|
|
44
|
+
severity=Severity.OK,
|
|
45
|
+
message="Redis server is running and responded to PING with PONG.",
|
|
46
|
+
check_id=f"{self.name}_ping_ok",
|
|
47
|
+
))
|
|
48
|
+
else:
|
|
49
|
+
diagnostics.append(Diagnostic(
|
|
50
|
+
title="Redis port open",
|
|
51
|
+
severity=Severity.OK,
|
|
52
|
+
message="Port 6379 is listening (Redis server active).",
|
|
53
|
+
check_id=f"{self.name}_port_ok",
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
elif is_redis_project:
|
|
57
|
+
start_cmd = "docker run -d -p 6379:6379 --name redis redis:alpine"
|
|
58
|
+
diagnostics.append(Diagnostic(
|
|
59
|
+
title="Redis not running",
|
|
60
|
+
severity=Severity.ERROR,
|
|
61
|
+
message="Redis server is not listening on default port 6379.",
|
|
62
|
+
why="The project uses Redis, but no local Redis instance is reachable on port 6379.",
|
|
63
|
+
evidence="Port 6379 connection refused.",
|
|
64
|
+
fix=f"Start Redis with Docker:\n {start_cmd}",
|
|
65
|
+
command=start_cmd,
|
|
66
|
+
confidence=Confidence.HIGH,
|
|
67
|
+
check_id=f"{self.name}_not_running",
|
|
68
|
+
))
|
|
69
|
+
|
|
70
|
+
return diagnostics
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Check virtual environment activation and isolation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from devdoctor.checks.base import Check, register_check
|
|
10
|
+
from devdoctor.models import Confidence, Diagnostic, FixAction, ScanContext, Severity
|
|
11
|
+
from devdoctor.utils.platform_info import (
|
|
12
|
+
find_local_venv,
|
|
13
|
+
get_venv_activation_command,
|
|
14
|
+
is_in_virtualenv,
|
|
15
|
+
is_windows,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@register_check
|
|
20
|
+
class VirtualenvCheck(Check):
|
|
21
|
+
name = "virtual_environment"
|
|
22
|
+
title = "Virtual Environment"
|
|
23
|
+
category = "environment"
|
|
24
|
+
description = "Verifies that the shell is operating within an active virtual environment."
|
|
25
|
+
|
|
26
|
+
def run(self, context: ScanContext) -> Diagnostic:
|
|
27
|
+
in_venv = is_in_virtualenv()
|
|
28
|
+
context.is_venv = in_venv
|
|
29
|
+
|
|
30
|
+
if in_venv:
|
|
31
|
+
venv_name = os.environ.get("VIRTUAL_ENV") or sys.prefix
|
|
32
|
+
venv_display = Path(venv_name).name if venv_name else "active"
|
|
33
|
+
return Diagnostic(
|
|
34
|
+
title=f"Virtual environment ({venv_display})",
|
|
35
|
+
severity=Severity.OK,
|
|
36
|
+
message="Current shell is using an active virtual environment.",
|
|
37
|
+
check_id=self.name,
|
|
38
|
+
metadata={"venv_path": venv_name},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Not in virtual environment
|
|
42
|
+
local_venv = find_local_venv(context.project_path)
|
|
43
|
+
venv_dir = local_venv.name if local_venv else ".venv"
|
|
44
|
+
|
|
45
|
+
if is_windows():
|
|
46
|
+
fix_text = f"Windows:\n {venv_dir}\\Scripts\\activate"
|
|
47
|
+
activate_cmd = f"{venv_dir}\\Scripts\\activate"
|
|
48
|
+
else:
|
|
49
|
+
fix_text = f"Linux / macOS:\n source {venv_dir}/bin/activate"
|
|
50
|
+
activate_cmd = f"source {venv_dir}/bin/activate"
|
|
51
|
+
|
|
52
|
+
if not local_venv:
|
|
53
|
+
create_cmd = f"{sys.executable} -m venv .venv"
|
|
54
|
+
fix_text = f"Create and activate virtualenv:\n {create_cmd}\n {activate_cmd}"
|
|
55
|
+
fix_action = FixAction(
|
|
56
|
+
description="Create .venv virtual environment",
|
|
57
|
+
command=create_cmd,
|
|
58
|
+
safe=True,
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
fix_action = FixAction(
|
|
62
|
+
description=f"Activate existing {venv_dir} environment",
|
|
63
|
+
command=activate_cmd,
|
|
64
|
+
safe=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return Diagnostic(
|
|
68
|
+
title="Virtual environment",
|
|
69
|
+
severity=Severity.ERROR,
|
|
70
|
+
message="Current shell is not using a virtual environment.",
|
|
71
|
+
why="Installing packages or running tasks outside a virtual environment pollutes the global Python installation and causes dependency collisions.",
|
|
72
|
+
evidence=f"Active interpreter is global: {sys.executable}",
|
|
73
|
+
fix=fix_text,
|
|
74
|
+
command=activate_cmd,
|
|
75
|
+
confidence=Confidence.HIGH,
|
|
76
|
+
fix_action=fix_action,
|
|
77
|
+
check_id=self.name,
|
|
78
|
+
metadata={"local_venv_found": local_venv is not None},
|
|
79
|
+
)
|
devdoctor/cli.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Command-line interface for DevDoctor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from devdoctor import __version__
|
|
13
|
+
from devdoctor.fixer import Fixer
|
|
14
|
+
from devdoctor.project import detect_project
|
|
15
|
+
from devdoctor.reporter import Reporter
|
|
16
|
+
from devdoctor.scanner import Scanner
|
|
17
|
+
|
|
18
|
+
# Ensure UTF-8 output on Windows terminals
|
|
19
|
+
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
20
|
+
try:
|
|
21
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
|
|
26
|
+
try:
|
|
27
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
console = Console(legacy_windows=False)
|
|
32
|
+
reporter = Reporter(console)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
36
|
+
"""Construct the command line argument parser for DevDoctor."""
|
|
37
|
+
parser = argparse.ArgumentParser(
|
|
38
|
+
prog="devdoctor",
|
|
39
|
+
description="Deterministic developer machine and project diagnostic doctor.",
|
|
40
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--version", "-V",
|
|
44
|
+
action="version",
|
|
45
|
+
version=f"DevDoctor {__version__}",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
|
|
49
|
+
|
|
50
|
+
# 1. scan (default command)
|
|
51
|
+
scan_parser = subparsers.add_parser("scan", help="Scan developer machine and project for issues.")
|
|
52
|
+
scan_parser.add_argument("path", nargs="?", default=".", help="Path to project directory (default: current directory)")
|
|
53
|
+
scan_parser.add_argument("--json", action="store_true", help="Output machine-readable JSON")
|
|
54
|
+
scan_parser.add_argument("--markdown", action="store_true", help="Output GitHub-flavored Markdown")
|
|
55
|
+
scan_parser.add_argument("-v", "--verbose", action="store_true", help="Show comprehensive diagnostic breakdown (WHY, EVIDENCE, FIX, COMMAND)")
|
|
56
|
+
scan_parser.add_argument("-o", "--output", help="Save diagnostic report to a specified file")
|
|
57
|
+
scan_parser.add_argument("--strict", action="store_true", help="Exit with code 1 if any warnings or errors are found")
|
|
58
|
+
scan_parser.add_argument("--checks-dir", help="Path to custom check plugins directory")
|
|
59
|
+
|
|
60
|
+
# 2. fix
|
|
61
|
+
fix_parser = subparsers.add_parser("fix", help="Interactively or automatically apply safe fixes for identified problems.")
|
|
62
|
+
fix_parser.add_argument("path", nargs="?", default=".", help="Path to project directory")
|
|
63
|
+
fix_parser.add_argument("--safe", action="store_true", help="Apply only safe, non-destructive automatic fixes")
|
|
64
|
+
fix_parser.add_argument("--dry-run", action="store_true", help="Show planned fixes without modifying anything")
|
|
65
|
+
fix_parser.add_argument("-y", "--yes", action="store_true", help="Confirm all fixes without interactive prompt")
|
|
66
|
+
|
|
67
|
+
# 3. dependencies
|
|
68
|
+
dep_parser = subparsers.add_parser("dependencies", help="Check for dependency conflicts and broken imports.")
|
|
69
|
+
dep_parser.add_argument("path", nargs="?", default=".", help="Path to project directory")
|
|
70
|
+
dep_parser.add_argument("-v", "--verbose", action="store_true", help="Show full evidence and fix commands")
|
|
71
|
+
dep_parser.add_argument("--json", action="store_true", help="Output dependency report as JSON")
|
|
72
|
+
|
|
73
|
+
# 4. port
|
|
74
|
+
port_parser = subparsers.add_parser("port", help="Network port tools and process management.")
|
|
75
|
+
port_sub = port_parser.add_subparsers(dest="port_action", required=True)
|
|
76
|
+
port_free_parser = port_sub.add_parser("free", help="Safely terminate process occupying a port.")
|
|
77
|
+
port_free_parser.add_argument("port_number", type=int, help="Port number to free (e.g. 8000)")
|
|
78
|
+
port_free_parser.add_argument("-y", "--yes", action="store_true", help="Terminate without interactive prompt")
|
|
79
|
+
|
|
80
|
+
# 5. project
|
|
81
|
+
proj_parser = subparsers.add_parser("project", help="Inspect detected project type, framework, and tech stack.")
|
|
82
|
+
proj_parser.add_argument("path", nargs="?", default=".", help="Path to project directory")
|
|
83
|
+
proj_parser.add_argument("--json", action="store_true", help="Output project profile as JSON")
|
|
84
|
+
|
|
85
|
+
# 6. report
|
|
86
|
+
rep_parser = subparsers.add_parser("report", help="Generate full diagnostic report in Markdown or JSON format.")
|
|
87
|
+
rep_parser.add_argument("path", nargs="?", default=".", help="Path to project directory")
|
|
88
|
+
rep_parser.add_argument("--format", choices=["terminal", "markdown", "json"], default="markdown", help="Output format")
|
|
89
|
+
rep_parser.add_argument("-o", "--output", help="Output file path (default: stdout)")
|
|
90
|
+
|
|
91
|
+
# 7. Framework commands
|
|
92
|
+
for fw in ["django", "fastapi", "docker", "postgres", "redis"]:
|
|
93
|
+
fw_parser = subparsers.add_parser(fw, help=f"Run diagnostics specifically for {fw.capitalize()}.")
|
|
94
|
+
fw_parser.add_argument("path", nargs="?", default=".", help="Path to project directory")
|
|
95
|
+
fw_parser.add_argument("-v", "--verbose", action="store_true", help="Show full diagnostics")
|
|
96
|
+
fw_parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
97
|
+
|
|
98
|
+
return parser
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_scan(args: argparse.Namespace) -> int:
|
|
102
|
+
"""Execute devdoctor scan."""
|
|
103
|
+
project_path = Path(args.path).resolve()
|
|
104
|
+
checks_dir = Path(args.checks_dir).resolve() if getattr(args, "checks_dir", None) else None
|
|
105
|
+
|
|
106
|
+
scanner = Scanner(project_path, custom_checks_dir=checks_dir)
|
|
107
|
+
report = scanner.scan()
|
|
108
|
+
|
|
109
|
+
if getattr(args, "json", False):
|
|
110
|
+
output = reporter.render_json(report)
|
|
111
|
+
if getattr(args, "output", None):
|
|
112
|
+
Path(args.output).write_text(output, encoding="utf-8")
|
|
113
|
+
console.print(f"[green]Report saved to {args.output}[/green]")
|
|
114
|
+
else:
|
|
115
|
+
print(output)
|
|
116
|
+
elif getattr(args, "markdown", False):
|
|
117
|
+
output = reporter.render_markdown(report)
|
|
118
|
+
if getattr(args, "output", None):
|
|
119
|
+
Path(args.output).write_text(output, encoding="utf-8")
|
|
120
|
+
console.print(f"[green]Report saved to {args.output}[/green]")
|
|
121
|
+
else:
|
|
122
|
+
print(output)
|
|
123
|
+
else:
|
|
124
|
+
reporter.render_terminal(report, verbose=getattr(args, "verbose", False))
|
|
125
|
+
if getattr(args, "output", None):
|
|
126
|
+
Path(args.output).write_text(reporter.render_markdown(report), encoding="utf-8")
|
|
127
|
+
console.print(f"[dim]Markdown copy saved to {args.output}[/dim]")
|
|
128
|
+
|
|
129
|
+
if getattr(args, "strict", False):
|
|
130
|
+
return 1 if report.total_problems > 0 else 0
|
|
131
|
+
return 1 if report.errors else 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_fix(args: argparse.Namespace) -> int:
|
|
135
|
+
"""Execute devdoctor fix."""
|
|
136
|
+
project_path = Path(args.path).resolve()
|
|
137
|
+
scanner = Scanner(project_path)
|
|
138
|
+
report = scanner.scan()
|
|
139
|
+
context = scanner.create_context()
|
|
140
|
+
|
|
141
|
+
fixer = Fixer(console)
|
|
142
|
+
plan = fixer.build_plan(report)
|
|
143
|
+
|
|
144
|
+
safe_only = getattr(args, "safe", False)
|
|
145
|
+
dry_run = getattr(args, "dry_run", False)
|
|
146
|
+
auto_yes = getattr(args, "yes", False)
|
|
147
|
+
|
|
148
|
+
res = fixer.execute_plan(
|
|
149
|
+
plan=plan,
|
|
150
|
+
context=context,
|
|
151
|
+
safe_only=safe_only,
|
|
152
|
+
dry_run=dry_run,
|
|
153
|
+
auto_yes=auto_yes,
|
|
154
|
+
interactive=not auto_yes and not dry_run,
|
|
155
|
+
)
|
|
156
|
+
return 0 if len(res.failed) == 0 else 1
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def cmd_dependencies(args: argparse.Namespace) -> int:
|
|
160
|
+
"""Execute devdoctor dependencies."""
|
|
161
|
+
project_path = Path(args.path).resolve()
|
|
162
|
+
scanner = Scanner(project_path)
|
|
163
|
+
report = scanner.scan(check_names={"dependency_conflicts", "broken_imports"})
|
|
164
|
+
|
|
165
|
+
if getattr(args, "json", False):
|
|
166
|
+
print(reporter.render_json(report))
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
console.print("[bold cyan]DevDoctor Dependency Diagnostics[/bold cyan]")
|
|
170
|
+
console.print("──────────────────────────────────────────\n")
|
|
171
|
+
|
|
172
|
+
if not report.problems:
|
|
173
|
+
console.print("[bold green]✓ No dependency conflicts or broken imports detected.[/bold green]")
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
for d in report.problems:
|
|
177
|
+
sym = "[red]❌[/red]" if d.is_error else "[yellow]⚠[/yellow]"
|
|
178
|
+
console.print(f"{sym} [bold]{d.title}[/bold]\n")
|
|
179
|
+
if d.evidence:
|
|
180
|
+
for line in d.evidence.splitlines():
|
|
181
|
+
console.print(f"{line}")
|
|
182
|
+
console.print()
|
|
183
|
+
elif d.message:
|
|
184
|
+
console.print(f"{d.message}\n")
|
|
185
|
+
|
|
186
|
+
if d.fix:
|
|
187
|
+
console.print(f"[bold]Suggested action:[/bold]\n{d.fix}\n")
|
|
188
|
+
|
|
189
|
+
return 1 if report.errors else 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def cmd_port(args: argparse.Namespace) -> int:
|
|
193
|
+
"""Execute devdoctor port free <port>."""
|
|
194
|
+
if args.port_action == "free":
|
|
195
|
+
port = args.port_number
|
|
196
|
+
auto_yes = getattr(args, "yes", False)
|
|
197
|
+
fixer = Fixer(console)
|
|
198
|
+
success = fixer.free_port_interactive(port, auto_yes=auto_yes)
|
|
199
|
+
return 0 if success else 1
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def cmd_project(args: argparse.Namespace) -> int:
|
|
204
|
+
"""Execute devdoctor project."""
|
|
205
|
+
project_path = Path(args.path).resolve()
|
|
206
|
+
info = detect_project(project_path)
|
|
207
|
+
|
|
208
|
+
if getattr(args, "json", False):
|
|
209
|
+
import json
|
|
210
|
+
print(json.dumps(info.to_dict(), indent=2))
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
console.print(f"[bold cyan]Project Profile: {info.name}[/bold cyan]")
|
|
214
|
+
console.print("──────────────────────────────────────────")
|
|
215
|
+
console.print(f"[bold]Detected Type:[/bold] {info.project_type or 'Generic Python'}")
|
|
216
|
+
console.print(f"[bold]Stack Summary:[/bold] {info.summary_stack}")
|
|
217
|
+
console.print(f"[bold]Technologies:[/bold] {', '.join(info.technologies) if info.technologies else 'None'}")
|
|
218
|
+
console.print(f"[bold]Frameworks:[/bold] {', '.join(info.frameworks) if info.frameworks else 'None'}")
|
|
219
|
+
console.print(f"[bold]Config Files:[/bold] {', '.join(info.config_files) if info.config_files else 'None'}")
|
|
220
|
+
console.print(f"[bold]Dependencies:[/bold] {len(info.dependencies)} declared")
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cmd_report(args: argparse.Namespace) -> int:
|
|
225
|
+
"""Execute devdoctor report."""
|
|
226
|
+
project_path = Path(args.path).resolve()
|
|
227
|
+
scanner = Scanner(project_path)
|
|
228
|
+
report = scanner.scan()
|
|
229
|
+
|
|
230
|
+
fmt = getattr(args, "format", "markdown")
|
|
231
|
+
if fmt == "json":
|
|
232
|
+
output = reporter.render_json(report)
|
|
233
|
+
elif fmt == "markdown":
|
|
234
|
+
output = reporter.render_markdown(report)
|
|
235
|
+
else:
|
|
236
|
+
reporter.render_terminal(report, verbose=True)
|
|
237
|
+
return 0
|
|
238
|
+
|
|
239
|
+
if getattr(args, "output", None):
|
|
240
|
+
Path(args.output).write_text(output, encoding="utf-8")
|
|
241
|
+
console.print(f"[bold green]Report saved to {args.output}[/bold green]")
|
|
242
|
+
else:
|
|
243
|
+
print(output)
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def cmd_framework(name: str, args: argparse.Namespace) -> int:
|
|
248
|
+
"""Execute framework diagnostic command."""
|
|
249
|
+
project_path = Path(args.path).resolve()
|
|
250
|
+
scanner = Scanner(project_path)
|
|
251
|
+
report = scanner.scan(check_names={name})
|
|
252
|
+
|
|
253
|
+
if getattr(args, "json", False):
|
|
254
|
+
print(reporter.render_json(report))
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
console.print(f"[bold cyan]DevDoctor: {name.capitalize()} Diagnostics[/bold cyan]")
|
|
258
|
+
console.print("──────────────────────────────────────────\n")
|
|
259
|
+
|
|
260
|
+
if not report.diagnostics:
|
|
261
|
+
console.print(f"[dim]No specific {name} checks triggered.[/dim]")
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
if report.passed:
|
|
265
|
+
for p in report.passed:
|
|
266
|
+
console.print(f"[green]✓[/green] {p.title}")
|
|
267
|
+
console.print()
|
|
268
|
+
|
|
269
|
+
if report.problems:
|
|
270
|
+
for d in report.problems:
|
|
271
|
+
sym = "[red]❌[/red]" if d.is_error else "[yellow]⚠[/yellow]"
|
|
272
|
+
console.print(f"{sym} [bold]{d.title}[/bold]")
|
|
273
|
+
if d.message:
|
|
274
|
+
console.print(f" {d.message}\n")
|
|
275
|
+
if d.evidence:
|
|
276
|
+
console.print(f" {d.evidence}\n")
|
|
277
|
+
if d.fix:
|
|
278
|
+
console.print(f" Fix:\n {d.fix}\n")
|
|
279
|
+
else:
|
|
280
|
+
console.print(f"[bold green]✓ All {name} diagnostics passed![/bold green]")
|
|
281
|
+
|
|
282
|
+
return 1 if report.errors else 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
286
|
+
"""CLI entry point for DevDoctor."""
|
|
287
|
+
if argv is None:
|
|
288
|
+
argv = sys.argv[1:]
|
|
289
|
+
|
|
290
|
+
# Default to 'scan' if no arguments or flags provided
|
|
291
|
+
if not argv:
|
|
292
|
+
argv = ["scan"]
|
|
293
|
+
elif argv[0].startswith("-") and argv[0] not in ("-h", "--help", "-V", "--version"):
|
|
294
|
+
argv = ["scan"] + argv
|
|
295
|
+
|
|
296
|
+
parser = create_parser()
|
|
297
|
+
args = parser.parse_args(argv)
|
|
298
|
+
|
|
299
|
+
if args.command == "scan":
|
|
300
|
+
return cmd_scan(args)
|
|
301
|
+
elif args.command == "fix":
|
|
302
|
+
return cmd_fix(args)
|
|
303
|
+
elif args.command == "dependencies":
|
|
304
|
+
return cmd_dependencies(args)
|
|
305
|
+
elif args.command == "port":
|
|
306
|
+
return cmd_port(args)
|
|
307
|
+
elif args.command == "project":
|
|
308
|
+
return cmd_project(args)
|
|
309
|
+
elif args.command == "report":
|
|
310
|
+
return cmd_report(args)
|
|
311
|
+
elif args.command in ("django", "fastapi", "docker", "postgres", "redis"):
|
|
312
|
+
return cmd_framework(args.command, args)
|
|
313
|
+
else:
|
|
314
|
+
parser.print_help()
|
|
315
|
+
return 0
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
if __name__ == "__main__":
|
|
319
|
+
sys.exit(main())
|