devdoctor 0.2.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 DevEnv Doctor Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: devdoctor
3
+ Version: 0.2.0
4
+ Summary: Professional CLI tool for developer environment diagnostics and auto-fixing
5
+ License-File: LICENSE
6
+ Keywords: cli,devops,environment,setup,diagnostics
7
+ Author: Starikov A.V.
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Build Tools
20
+ Provides-Extra: ai
21
+ Requires-Dist: click (>=8.1.7,<9.0.0)
22
+ Requires-Dist: openai (>=1.12.0,<2.0.0) ; extra == "ai"
23
+ Requires-Dist: psutil (>=5.9.8,<6.0.0)
24
+ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
25
+ Requires-Dist: rich (>=13.7.0,<14.0.0)
26
+ Project-URL: Documentation, https://AttackBeaver.github.io/devdoctor
27
+ Project-URL: Homepage, https://github.com/AttackBeaver/devdoctor
28
+ Project-URL: Repository, https://github.com/AttackBeaver/devdoctor
29
+ Description-Content-Type: text/markdown
30
+
31
+ # 🩺 DevEnv Doctor
32
+
33
+ [![CI](https://github.com/AttackBeaver/devdoctor/actions/workflows/ci.yml/badge.svg)](https://github.com/AttackBeaver/devdoctor/actions)
34
+ [![PyPI version](https://img.shields.io/pypi/v/devdoctor.svg)](https://pypi.org/project/devdoctor/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
36
+
37
+ **DevEnv Doctor** β€” это ΠΏΡ€ΠΎΡ„Π΅ΡΡΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ CLI-инструмСнт для автоматичСской диагностики ΠΈ настройки окруТСния Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ°. Он провСряСт инструмСнты, ΠΏΠΎΡ€Ρ‚Ρ‹, ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния ΠΈ ΠΏΠΎΠΌΠΎΠ³Π°Π΅Ρ‚ ΠΈΡΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹ ΠΎΠ΄Π½ΠΎΠΉ ΠΊΠΎΠΌΠ°Π½Π΄ΠΎΠΉ.
38
+
39
+ ## πŸš€ Быстрый старт
40
+
41
+ 1. **Установка**:
42
+ ```bash
43
+ pip install devdoctor
44
+ # ΠΈΠ»ΠΈ с ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ AI совСтов
45
+ pip install "devdoctor[ai]"
46
+ ```
47
+
48
+ 2. **Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ**:
49
+ ```bash
50
+ devdoctor init
51
+ ```
52
+
53
+ 3. **Запуск диагностики**:
54
+ ```bash
55
+ devdoctor check
56
+ ```
57
+
58
+ 4. **Авто-исправлСниС**:
59
+ ```bash
60
+ devdoctor check --fix
61
+ ```
62
+
63
+ ## πŸ›  ВозмоТности
64
+
65
+ - **ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° инструмСнтов**: НаличиС ΠΈ вСрсии Git, Python, Docker, Node.js ΠΈ Π΄Ρ€.
66
+ - **Π‘Π²ΠΎΠ±ΠΎΠ΄Π½Ρ‹Π΅ ΠΏΠΎΡ€Ρ‚Ρ‹**: Поиск процСссов, Π·Π°Π½ΠΈΠΌΠ°ΡŽΡ‰ΠΈΡ… ΠΏΠΎΡ€Ρ‚Ρ‹ (PostgreSQL, Redis, ΠΈ Ρ‚.Π΄.).
67
+ - **ДисковоС пространство**: ΠšΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒ свободного мСста ΠΈ наличия Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹Ρ… ΠΏΠ°ΠΏΠΎΠΊ.
68
+ - **Environment**: ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° `.env` Ρ„Π°ΠΉΠ»ΠΎΠ² ΠΈ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ….
69
+ - **Custom Checks**: Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π΄ΠΎΠ±Π°Π²Π»ΡΡ‚ΡŒ свои ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ прямо Π² YAML.
70
+ - **AI Advisor**: ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ совСтов ΠΏΠΎ ΠΈΡΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΡŽ ошибок Ρ‡Π΅Ρ€Π΅Π· GPT-4.
71
+
72
+ ## βš™οΈ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ (.devdoctor.yaml)
73
+
74
+ ```yaml
75
+ tools:
76
+ - name: git
77
+ min_version: "2.30.0"
78
+
79
+ ports:
80
+ - number: 5432
81
+ description: "PostgreSQL"
82
+
83
+ env_files: [".env"]
84
+ required_env_vars: ["DATABASE_URL", "SECRET_KEY"]
85
+
86
+ custom_checks:
87
+ - name: "Check Redis"
88
+ command: "redis-cli ping"
89
+ expected_output_contains: "PONG"
90
+ ```
91
+
92
+ ## πŸ‘¨β€πŸ’» Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ°
93
+
94
+ Π˜Π½ΡΡ‚Ρ€ΡƒΠΊΡ†ΠΈΠΈ ΠΏΠΎ Ρ€Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΡŽ ΠΈ Ρ‚Π΅ΡΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡŽ находятся Π² [CONTRIBUTING.md](docs/development.md).
95
+
96
+ ---
97
+ **Автор**: [Π‘Ρ‚Π°Ρ€ΠΈΠΊΠΎΠ² А.Π’.](https://github.com/AttackBeaver) β€” ΠΏΡ€Π΅ΠΏΠΎΠ΄Π°Π²Π°Ρ‚Π΅Π»ΡŒ Π‘ΠŸΠžΠ£ ОО "БПК"
98
+ **GitHub**: [AttackBeaver/devdoctor](https://github.com/AttackBeaver/devdoctor)
99
+ **ЛицСнзия**: MIT
100
+ ## Установка
101
+
102
+ ```bash
103
+ poetry install
104
+ ```
105
+
106
+ ## ИспользованиС
107
+
108
+ ```bash
109
+ # Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ ΠΊΠΎΠ½Ρ„ΠΈΠ³Π°
110
+ poetry run devdoctor init
111
+
112
+ # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° окруТСния
113
+ poetry run devdoctor check
114
+
115
+ # ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° исправлСния (MVP: Π·Π°Π³Π»ΡƒΡˆΠΊΠ°)
116
+ poetry run devdoctor check --fix
117
+ ```
118
+
@@ -0,0 +1,87 @@
1
+ # 🩺 DevEnv Doctor
2
+
3
+ [![CI](https://github.com/AttackBeaver/devdoctor/actions/workflows/ci.yml/badge.svg)](https://github.com/AttackBeaver/devdoctor/actions)
4
+ [![PyPI version](https://img.shields.io/pypi/v/devdoctor.svg)](https://pypi.org/project/devdoctor/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ **DevEnv Doctor** β€” это ΠΏΡ€ΠΎΡ„Π΅ΡΡΠΈΠΎΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ CLI-инструмСнт для автоматичСской диагностики ΠΈ настройки окруТСния Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ°. Он провСряСт инструмСнты, ΠΏΠΎΡ€Ρ‚Ρ‹, ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ окруТСния ΠΈ ΠΏΠΎΠΌΠΎΠ³Π°Π΅Ρ‚ ΠΈΡΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹ ΠΎΠ΄Π½ΠΎΠΉ ΠΊΠΎΠΌΠ°Π½Π΄ΠΎΠΉ.
8
+
9
+ ## πŸš€ Быстрый старт
10
+
11
+ 1. **Установка**:
12
+ ```bash
13
+ pip install devdoctor
14
+ # ΠΈΠ»ΠΈ с ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ AI совСтов
15
+ pip install "devdoctor[ai]"
16
+ ```
17
+
18
+ 2. **Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ**:
19
+ ```bash
20
+ devdoctor init
21
+ ```
22
+
23
+ 3. **Запуск диагностики**:
24
+ ```bash
25
+ devdoctor check
26
+ ```
27
+
28
+ 4. **Авто-исправлСниС**:
29
+ ```bash
30
+ devdoctor check --fix
31
+ ```
32
+
33
+ ## πŸ›  ВозмоТности
34
+
35
+ - **ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° инструмСнтов**: НаличиС ΠΈ вСрсии Git, Python, Docker, Node.js ΠΈ Π΄Ρ€.
36
+ - **Π‘Π²ΠΎΠ±ΠΎΠ΄Π½Ρ‹Π΅ ΠΏΠΎΡ€Ρ‚Ρ‹**: Поиск процСссов, Π·Π°Π½ΠΈΠΌΠ°ΡŽΡ‰ΠΈΡ… ΠΏΠΎΡ€Ρ‚Ρ‹ (PostgreSQL, Redis, ΠΈ Ρ‚.Π΄.).
37
+ - **ДисковоС пространство**: ΠšΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒ свободного мСста ΠΈ наличия Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹Ρ… ΠΏΠ°ΠΏΠΎΠΊ.
38
+ - **Environment**: ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° `.env` Ρ„Π°ΠΉΠ»ΠΎΠ² ΠΈ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ….
39
+ - **Custom Checks**: Π’ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ Π΄ΠΎΠ±Π°Π²Π»ΡΡ‚ΡŒ свои ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ прямо Π² YAML.
40
+ - **AI Advisor**: ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ совСтов ΠΏΠΎ ΠΈΡΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΡŽ ошибок Ρ‡Π΅Ρ€Π΅Π· GPT-4.
41
+
42
+ ## βš™οΈ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ (.devdoctor.yaml)
43
+
44
+ ```yaml
45
+ tools:
46
+ - name: git
47
+ min_version: "2.30.0"
48
+
49
+ ports:
50
+ - number: 5432
51
+ description: "PostgreSQL"
52
+
53
+ env_files: [".env"]
54
+ required_env_vars: ["DATABASE_URL", "SECRET_KEY"]
55
+
56
+ custom_checks:
57
+ - name: "Check Redis"
58
+ command: "redis-cli ping"
59
+ expected_output_contains: "PONG"
60
+ ```
61
+
62
+ ## πŸ‘¨β€πŸ’» Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ°
63
+
64
+ Π˜Π½ΡΡ‚Ρ€ΡƒΠΊΡ†ΠΈΠΈ ΠΏΠΎ Ρ€Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΡŽ ΠΈ Ρ‚Π΅ΡΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡŽ находятся Π² [CONTRIBUTING.md](docs/development.md).
65
+
66
+ ---
67
+ **Автор**: [Π‘Ρ‚Π°Ρ€ΠΈΠΊΠΎΠ² А.Π’.](https://github.com/AttackBeaver) β€” ΠΏΡ€Π΅ΠΏΠΎΠ΄Π°Π²Π°Ρ‚Π΅Π»ΡŒ Π‘ΠŸΠžΠ£ ОО "БПК"
68
+ **GitHub**: [AttackBeaver/devdoctor](https://github.com/AttackBeaver/devdoctor)
69
+ **ЛицСнзия**: MIT
70
+ ## Установка
71
+
72
+ ```bash
73
+ poetry install
74
+ ```
75
+
76
+ ## ИспользованиС
77
+
78
+ ```bash
79
+ # Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ ΠΊΠΎΠ½Ρ„ΠΈΠ³Π°
80
+ poetry run devdoctor init
81
+
82
+ # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° окруТСния
83
+ poetry run devdoctor check
84
+
85
+ # ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° исправлСния (MVP: Π·Π°Π³Π»ΡƒΡˆΠΊΠ°)
86
+ poetry run devdoctor check --fix
87
+ ```
@@ -0,0 +1,52 @@
1
+ [tool.poetry]
2
+ name = "devdoctor"
3
+ version = "0.2.0"
4
+ description = "Professional CLI tool for developer environment diagnostics and auto-fixing"
5
+ authors = ["Starikov A.V."]
6
+ readme = "README.md"
7
+ homepage = "https://github.com/AttackBeaver/devdoctor"
8
+ repository = "https://github.com/AttackBeaver/devdoctor"
9
+ documentation = "https://AttackBeaver.github.io/devdoctor"
10
+ keywords = ["cli", "devops", "environment", "setup", "diagnostics"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "Topic :: Software Development :: Build Tools",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3.9",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+ packages = [{include = "devdoctor", from = "src"}]
22
+
23
+ [tool.poetry.dependencies]
24
+ python = "^3.9"
25
+ click = "^8.1.7"
26
+ rich = "^13.7.0"
27
+ pyyaml = "^6.0.1"
28
+ psutil = "^5.9.8"
29
+ openai = {version = "^1.12.0", optional = true}
30
+
31
+ [tool.poetry.extras]
32
+ ai = ["openai"]
33
+
34
+ [tool.poetry.group.dev.dependencies]
35
+ pytest = "^8.0.0"
36
+ pytest-mock = "^3.12.0"
37
+ black = "^24.2.0"
38
+ isort = "^5.13.2"
39
+ flake8 = "^7.0.0"
40
+ mypy = "^1.8.0"
41
+ pre-commit = "^3.6.1"
42
+ types-psutil = "^5.9.5.20240205"
43
+ types-PyYAML = "^6.0.12.12"
44
+ mkdocs = "^1.5.3"
45
+ mkdocs-material = "^9.5.10"
46
+
47
+ [tool.poetry.scripts]
48
+ devdoctor = "devdoctor.cli:main"
49
+
50
+ [build-system]
51
+ requires = ["poetry-core"]
52
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,32 @@
1
+ import os
2
+ from typing import List, Dict
3
+
4
+ def get_ai_suggestions(results: List[Dict]) -> str:
5
+ try:
6
+ from openai import OpenAI
7
+ except ImportError:
8
+ return "OpenAI library not installed. Use 'pip install devdoctor[ai]'."
9
+
10
+ api_key = os.getenv("OPENAI_API_KEY")
11
+ if not api_key:
12
+ return "OPENAI_API_KEY not found in environment."
13
+
14
+ client = OpenAI(api_key=api_key)
15
+
16
+ failed_checks = [r for r in results if r["status"] != "OK"]
17
+ if not failed_checks:
18
+ return "Everything looks great! No AI suggestions needed."
19
+
20
+ prompt = "I have the following issues in my development environment:\n"
21
+ for r in failed_checks:
22
+ prompt += f"- {r['check']}: {r['message']}\n"
23
+ prompt += "\nProvide concise, expert advice on how to fix these issues."
24
+
25
+ try:
26
+ response = client.chat.completions.create(
27
+ model="gpt-3.5-turbo",
28
+ messages=[{"role": "user", "content": prompt}]
29
+ )
30
+ return response.choices[0].message.content or "No suggestions received."
31
+ except Exception as e:
32
+ return f"AI Error: {str(e)}"
@@ -0,0 +1,7 @@
1
+ from .tools import ToolCheck
2
+ from .ports import PortsCheck
3
+ from .disk import DiskCheck
4
+ from .path import PathCheck
5
+ from .env import EnvCheck
6
+
7
+ __all__ = ["ToolCheck", "PortsCheck", "DiskCheck", "PathCheck", "EnvCheck"]
@@ -0,0 +1,16 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class Check(ABC):
4
+ def __init__(self, name: str, description: str):
5
+ self.name = name
6
+ self.description = description
7
+
8
+ @abstractmethod
9
+ def run(self) -> tuple[bool, str]:
10
+ """Returns (success, message)"""
11
+ pass
12
+
13
+ @abstractmethod
14
+ def fix(self) -> bool:
15
+ """Attempts to fix the issue. Returns True if fixed."""
16
+ pass
@@ -0,0 +1,49 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from typing import Tuple
4
+ from .base import Check
5
+
6
+ class CustomCheck(Check):
7
+ def __init__(self, config: dict):
8
+ name = config.get("name", "Custom Check")
9
+ super().__init__(name=name, description=config.get("description", ""))
10
+ self.config = config
11
+ self.check_type = config.get("type", "command")
12
+
13
+ def run(self) -> Tuple[bool, str]:
14
+ if self.check_type == "command":
15
+ return self._run_command()
16
+ elif self.check_type == "file_exists":
17
+ return self._run_file_exists()
18
+ return False, f"Unknown check type: {self.check_type}"
19
+
20
+ def _run_command(self) -> Tuple[bool, str]:
21
+ cmd = self.config.get("command")
22
+ expected = self.config.get("expected_output_contains")
23
+ try:
24
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
25
+ if result.returncode == 0:
26
+ if not expected or expected in result.stdout:
27
+ return True, "Command executed successfully"
28
+ return False, f"Output mismatch. Expected: {expected}"
29
+ return False, f"Command failed with exit code {result.returncode}"
30
+ except Exception as e:
31
+ return False, str(e)
32
+
33
+ def _run_file_exists(self) -> Tuple[bool, str]:
34
+ path = Path(self.config.get("path", ""))
35
+ should_exist = self.config.get("should_exist", True)
36
+ exists = path.exists()
37
+ if exists == should_exist:
38
+ return True, f"Path {path} {'exists' if exists else 'does not exist'} as expected"
39
+ return False, f"Path {path} {'exists' if exists else 'does not exist'} (unexpected)"
40
+
41
+ def fix(self) -> bool:
42
+ fix_cmd = self.config.get("fix_command") or self.config.get("fix")
43
+ if not fix_cmd:
44
+ return False
45
+ try:
46
+ subprocess.run(fix_cmd, shell=True, check=True)
47
+ return True
48
+ except Exception:
49
+ return False
@@ -0,0 +1,34 @@
1
+ import shutil
2
+ from pathlib import Path
3
+ from .base import Check
4
+ from ..fixers.create_dirs import ensure_directories
5
+
6
+ class DiskCheck(Check):
7
+ def __init__(self, path: str, min_gb: float = 1.0, dirs_to_create: list[str] = None):
8
+ super().__init__(
9
+ name=f"Disk: {path}",
10
+ description=f"Check free space (min {min_gb}GB) and required directories"
11
+ )
12
+ self.path = path
13
+ self.min_gb = min_gb
14
+ self.dirs_to_create = dirs_to_create or []
15
+
16
+ def run(self) -> tuple[bool, str]:
17
+ try:
18
+ usage = shutil.disk_usage(self.path)
19
+ free_gb = usage.free / (1024**3)
20
+ if free_gb < self.min_gb:
21
+ return False, f"Low disk space: {free_gb:.2f}GB free (required {self.min_gb}GB)"
22
+
23
+ missing = [d for d in self.dirs_to_create if not Path(d).exists()]
24
+ if missing:
25
+ return False, f"Missing directories: {', '.join(missing)}"
26
+
27
+ return True, f"{free_gb:.2f}GB free, all directories exist"
28
+ except FileNotFoundError:
29
+ return False, f"Path not found: {self.path}"
30
+
31
+ def fix(self) -> bool:
32
+ if self.dirs_to_create:
33
+ return ensure_directories(self.dirs_to_create)
34
+ return False
@@ -0,0 +1,30 @@
1
+ from pathlib import Path
2
+ from .base import Check
3
+ from ..fixers.env_generator import generate_env_if_missing
4
+
5
+ class EnvCheck(Check):
6
+ def __init__(self, env_path: str, required_vars: list[str]):
7
+ super().__init__(
8
+ name=f"Env: {env_path}",
9
+ description=f"Check if {env_path} exists and contains required variables"
10
+ )
11
+ self.env_path = Path(env_path)
12
+ self.required_vars = required_vars
13
+
14
+ def run(self) -> tuple[bool, str]:
15
+ if not self.env_path.exists():
16
+ return False, f"File {self.env_path} is missing"
17
+
18
+ content = self.env_path.read_text()
19
+ missing = []
20
+ for var in self.required_vars:
21
+ if f"{var}=" not in content:
22
+ missing.append(var)
23
+
24
+ if missing:
25
+ return False, f"Missing variables: {', '.join(missing)}"
26
+
27
+ return True, "All required variables present"
28
+
29
+ def fix(self) -> bool:
30
+ return generate_env_if_missing(str(self.env_path), self.required_vars)
@@ -0,0 +1,19 @@
1
+ import shutil
2
+ from .base import Check
3
+
4
+ class PathCheck(Check):
5
+ def __init__(self, bin_name: str):
6
+ super().__init__(
7
+ name=f"PATH: {bin_name}",
8
+ description=f"Check if {bin_name} is in PATH and executable"
9
+ )
10
+ self.bin_name = bin_name
11
+
12
+ def run(self) -> tuple[bool, str]:
13
+ path = shutil.which(self.bin_name)
14
+ if path:
15
+ return True, f"Found at: {path}"
16
+ return False, f"'{self.bin_name}' not found in PATH"
17
+
18
+ def fix(self) -> bool:
19
+ return False
@@ -0,0 +1,24 @@
1
+ import psutil
2
+ from .base import Check
3
+ from ..fixers.kill_port import kill_process_on_port
4
+
5
+ class PortsCheck(Check):
6
+ def __init__(self, port: int, description: str = ""):
7
+ super().__init__(
8
+ name=f"Port: {port}",
9
+ description=f"Check if port {port} ({description}) is free"
10
+ )
11
+ self.port = port
12
+
13
+ def run(self) -> tuple[bool, str]:
14
+ for conn in psutil.net_connections():
15
+ if conn.laddr.port == self.port and conn.status == 'LISTEN':
16
+ try:
17
+ process = psutil.Process(conn.pid)
18
+ return False, f"Port {self.port} is occupied by '{process.name()}' (PID: {conn.pid})"
19
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
20
+ return False, f"Port {self.port} is occupied (PID: {conn.pid})"
21
+ return True, f"Port {self.port} is free"
22
+
23
+ def fix(self) -> bool:
24
+ return kill_process_on_port(self.port)
@@ -0,0 +1,23 @@
1
+ from .base import Check
2
+ from ..utils.system import get_tool_version
3
+
4
+ class ToolCheck(Check):
5
+ def __init__(self, tool_name: str, min_version: str = None):
6
+ super().__init__(
7
+ name=f"Tool: {tool_name}",
8
+ description=f"Check if {tool_name} is installed (min version: {min_version or 'any'})"
9
+ )
10
+ self.tool_name = tool_name
11
+ self.min_version = min_version
12
+
13
+ def run(self) -> tuple[bool, str]:
14
+ version = get_tool_version(self.tool_name)
15
+ if not version:
16
+ return False, f"{self.tool_name} is not installed"
17
+
18
+ # Π’ MVP просто провСряСм Π½Π°Π»ΠΈΡ‡ΠΈΠ΅. Π‘Ρ€Π°Π²Π½Π΅Π½ΠΈΠ΅ вСрсий ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΏΠΎΠ·ΠΆΠ΅.
19
+ return True, f"Found: {version}"
20
+
21
+ def fix(self) -> bool:
22
+ # Авто-фикс для инструмСнтов ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ Ρ‚Ρ€Π΅Π±ΡƒΠ΅Ρ‚ ΠΏΠ°ΠΊΠ΅Ρ‚Π½ΠΎΠ³ΠΎ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€Π° (brew, apt)
23
+ return False
@@ -0,0 +1,110 @@
1
+ import click
2
+ import shutil
3
+ from pathlib import Path
4
+ from typing import List, Any
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+ from rich.panel import Panel
7
+
8
+ from .config import load_config, DEFAULT_CONFIG_NAME
9
+ from .reporter import Reporter
10
+ from .checks import ToolCheck, PortsCheck, DiskCheck, PathCheck, EnvCheck
11
+ from .checks.custom import CustomCheck
12
+ from .checks.base import Check
13
+
14
+ @click.group()
15
+ @click.version_option()
16
+ def main() -> None:
17
+ """DevEnv Doctor: Professional diagnostic tool for your development environment."""
18
+ pass
19
+
20
+ @main.command()
21
+ @click.option('--fix', is_flag=True, help='Try to fix issues automatically.')
22
+ @click.option('--verbose', is_flag=True, help='Show detailed debug info.')
23
+ @click.option('--quiet', is_flag=True, help='Only show summary.')
24
+ @click.option('--ai', is_flag=True, help='Get AI suggestions for failed checks.')
25
+ def check(fix: bool, verbose: bool, quiet: bool, ai: bool) -> None:
26
+ """Run environment diagnostics and optionally fix issues."""
27
+ config = load_config()
28
+ reporter = Reporter()
29
+ checks: List[Check] = []
30
+
31
+ # 1. Tools
32
+ for t in config.get('tools', []):
33
+ checks.append(ToolCheck(t['name'], t.get('min_version')))
34
+ checks.append(PathCheck(t['name']))
35
+
36
+ # 2. Ports
37
+ for p in config.get('ports', []):
38
+ port_num = p if isinstance(p, int) else p.get('number')
39
+ desc = "" if isinstance(p, int) else p.get('description', '')
40
+ checks.append(PortsCheck(port_num, desc))
41
+
42
+ # 3. Disk & Dirs
43
+ dirs = config.get('directories_to_create', [])
44
+ for path in config.get('disk_paths', ['.']):
45
+ checks.append(DiskCheck(path, dirs_to_create=dirs))
46
+
47
+ # 4. Env
48
+ env_vars = config.get('required_env_vars', [])
49
+ for env_file in config.get('env_files', ['.env']):
50
+ checks.append(EnvCheck(env_file, env_vars))
51
+
52
+ # 5. Custom
53
+ for cc in config.get('custom_checks', []):
54
+ checks.append(CustomCheck(cc))
55
+
56
+ with Progress(
57
+ SpinnerColumn(),
58
+ TextColumn("[progress.description]{task.description}"),
59
+ transient=True,
60
+ ) as progress:
61
+ task = progress.add_task("[cyan]Running diagnostics...", total=len(checks))
62
+
63
+ for c in checks:
64
+ if verbose:
65
+ progress.console.print(f"Checking: [bold]{c.name}[/bold]")
66
+
67
+ success, message = c.run()
68
+ if not success and fix:
69
+ if c.fix():
70
+ reporter.add_fix(c.name, True)
71
+ success, message = c.run() # Re-run after fix
72
+ else:
73
+ reporter.add_fix(c.name, False)
74
+
75
+ status = "OK" if success else "FAIL"
76
+ reporter.add_result(c.name, status, message)
77
+ progress.advance(task)
78
+
79
+ if not quiet:
80
+ reporter.print_report()
81
+ else:
82
+ failed = [r for r in reporter.results if r["status"] != "OK"]
83
+ click.echo(f"Status: {'FAIL' if failed else 'OK'} ({len(failed)} issues)")
84
+
85
+ if ai:
86
+ from .ai_advisor import get_ai_suggestions
87
+ with progress.console.status("[bold yellow]Consulting AI advisor..."):
88
+ advice = get_ai_suggestions(reporter.results)
89
+ progress.console.print(Panel(advice, title="AI Suggestions", border_style="yellow"))
90
+
91
+ @main.command()
92
+ def init() -> None:
93
+ """Initialize .devdoctor.yaml from example template."""
94
+ example = Path(".devdoctor.yaml.example")
95
+ target = Path(DEFAULT_CONFIG_NAME)
96
+
97
+ if target.exists():
98
+ click.confirm(f"{DEFAULT_CONFIG_NAME} already exists. Overwrite?", abort=True)
99
+
100
+ if example.exists():
101
+ shutil.copy(example, target)
102
+ click.echo(f"Successfully created {DEFAULT_CONFIG_NAME} from example.")
103
+ else:
104
+ with open(target, 'w', encoding='utf-8') as f:
105
+ f.write("tools:\n - name: git\n")
106
+ click.echo(f"Created default {DEFAULT_CONFIG_NAME} (example not found).")
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
@@ -0,0 +1,27 @@
1
+ import yaml
2
+ from pathlib import Path
3
+ from typing import Any, Dict
4
+
5
+ DEFAULT_CONFIG_NAME = ".devdoctor.yaml"
6
+
7
+ def load_config(path: str | Path | None = None) -> Dict[str, Any]:
8
+ config_path = Path(path or DEFAULT_CONFIG_NAME)
9
+ if not config_path.exists():
10
+ return {}
11
+
12
+ with open(config_path, 'r', encoding='utf-8') as f:
13
+ config = yaml.safe_load(f) or {}
14
+
15
+ # Π”Π΅Ρ„ΠΎΠ»Ρ‚Π½Ρ‹Π΅ значСния для Π½ΠΎΠ²Ρ‹Ρ… ΠΊΠ»ΡŽΡ‡Π΅ΠΉ
16
+ defaults = {
17
+ "tools": [],
18
+ "ports": [],
19
+ "disk_paths": ["."],
20
+ "directories_to_create": [],
21
+ "env_files": [".env"],
22
+ "required_env_vars": []
23
+ }
24
+ for key, value in defaults.items():
25
+ config.setdefault(key, value)
26
+
27
+ return config
@@ -0,0 +1 @@
1
+ # Placeholder for future fixers
@@ -0,0 +1,11 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ def ensure_directories(dirs: list[str]) -> bool:
5
+ success = True
6
+ for d in dirs:
7
+ try:
8
+ Path(d).mkdir(parents=True, exist_ok=True)
9
+ except Exception:
10
+ success = False
11
+ return success
@@ -0,0 +1,20 @@
1
+ from pathlib import Path
2
+
3
+ def generate_env_if_missing(env_path: str, required_vars: list[str]) -> bool:
4
+ path = Path(env_path)
5
+ try:
6
+ existing_content = path.read_text() if path.exists() else ""
7
+ new_lines = []
8
+
9
+ for var in required_vars:
10
+ if f"{var}=" not in existing_content:
11
+ new_lines.append(f"{var}=YOUR_{var}_HERE")
12
+
13
+ if new_lines:
14
+ with open(path, "a") as f:
15
+ if existing_content and not existing_content.endswith("\n"):
16
+ f.write("\n")
17
+ f.write("\n".join(new_lines) + "\n")
18
+ return True
19
+ except Exception:
20
+ return False
@@ -0,0 +1,15 @@
1
+ import psutil
2
+ import click
3
+
4
+ def kill_process_on_port(port: int) -> bool:
5
+ for conn in psutil.net_connections():
6
+ if conn.laddr.port == port and conn.status == 'LISTEN':
7
+ try:
8
+ process = psutil.Process(conn.pid)
9
+ if click.confirm(f"Kill process '{process.name()}' (PID: {conn.pid}) on port {port}?", default=False):
10
+ process.terminate()
11
+ process.wait(timeout=3)
12
+ return True
13
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.TimeoutExpired):
14
+ return False
15
+ return False
@@ -0,0 +1,50 @@
1
+ from rich.console import Console
2
+ from rich.table import Table
3
+ from rich.panel import Panel
4
+
5
+ class Reporter:
6
+ def __init__(self):
7
+ self.console = Console()
8
+ self.results = []
9
+ self.fixes = []
10
+
11
+ def add_result(self, check_name: str, status: str, message: str):
12
+ self.results.append({
13
+ "check": check_name,
14
+ "status": status,
15
+ "message": message
16
+ })
17
+
18
+ def add_fix(self, check_name: str, success: bool):
19
+ self.fixes.append({
20
+ "check": check_name,
21
+ "success": success
22
+ })
23
+
24
+ def print_report(self):
25
+ table = Table(title="DevEnv Doctor Report")
26
+ table.add_column("Check", style="cyan")
27
+ table.add_column("Status", style="bold")
28
+ table.add_column("Message")
29
+
30
+ for res in self.results:
31
+ color = "green" if res["status"] == "OK" else "red"
32
+ table.add_row(res["check"], f"[{color}]{res['status']}[/{color}]", res["message"])
33
+
34
+ self.console.print(table)
35
+
36
+ if self.fixes:
37
+ fix_table = Table(title="Fixes Applied")
38
+ fix_table.add_column("Check", style="cyan")
39
+ fix_table.add_column("Result", style="bold")
40
+ for fix in self.fixes:
41
+ color = "green" if fix["success"] else "yellow"
42
+ status = "FIXED" if fix["success"] else "FAILED"
43
+ fix_table.add_row(fix["check"], f"[{color}]{status}[/{color}]")
44
+ self.console.print(fix_table)
45
+
46
+ failed = [r for r in self.results if r["status"] != "OK"]
47
+ if failed and not self.fixes:
48
+ self.console.print(Panel(f"Found {len(failed)} issues!", style="red"))
49
+ elif not failed:
50
+ self.console.print(Panel("All checks passed!", style="green"))
@@ -0,0 +1,30 @@
1
+ import shutil
2
+ import subprocess
3
+ import socket
4
+ import psutil
5
+ from pathlib import Path
6
+
7
+ def get_tool_version(tool: str) -> str | None:
8
+ try:
9
+ result = subprocess.run(
10
+ [tool, "--version"],
11
+ capture_output=True,
12
+ text=True,
13
+ check=False
14
+ )
15
+ output = result.stdout.strip() or result.stderr.strip()
16
+ # ΠŸΡ€ΠΎΡΡ‚ΠΎΠΉ парсинг ΠΏΠ΅Ρ€Π²ΠΎΠΉ строки (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, "git version 2.34.1")
17
+ return output.split('\n')[0] if output else None
18
+ except FileNotFoundError:
19
+ return None
20
+
21
+ def is_port_open(port: int) -> bool:
22
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
23
+ return s.connect_ex(('localhost', port)) == 0
24
+
25
+ def get_free_disk_space(path: str = ".") -> float:
26
+ usage = psutil.disk_usage(path)
27
+ return usage.free / (1024**3) # GB
28
+
29
+ def check_path_for_binary(bin_name: str) -> bool:
30
+ return shutil.which(bin_name) is not None