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.
- devdoctor-0.2.0/LICENSE +21 -0
- devdoctor-0.2.0/PKG-INFO +118 -0
- devdoctor-0.2.0/README.md +87 -0
- devdoctor-0.2.0/pyproject.toml +52 -0
- devdoctor-0.2.0/src/devdoctor/__init__.py +1 -0
- devdoctor-0.2.0/src/devdoctor/ai_advisor.py +32 -0
- devdoctor-0.2.0/src/devdoctor/checks/__init__.py +7 -0
- devdoctor-0.2.0/src/devdoctor/checks/base.py +16 -0
- devdoctor-0.2.0/src/devdoctor/checks/custom.py +49 -0
- devdoctor-0.2.0/src/devdoctor/checks/disk.py +34 -0
- devdoctor-0.2.0/src/devdoctor/checks/env.py +30 -0
- devdoctor-0.2.0/src/devdoctor/checks/path.py +19 -0
- devdoctor-0.2.0/src/devdoctor/checks/ports.py +24 -0
- devdoctor-0.2.0/src/devdoctor/checks/tools.py +23 -0
- devdoctor-0.2.0/src/devdoctor/cli.py +110 -0
- devdoctor-0.2.0/src/devdoctor/config.py +27 -0
- devdoctor-0.2.0/src/devdoctor/fixers/__init__.py +1 -0
- devdoctor-0.2.0/src/devdoctor/fixers/create_dirs.py +11 -0
- devdoctor-0.2.0/src/devdoctor/fixers/env_generator.py +20 -0
- devdoctor-0.2.0/src/devdoctor/fixers/kill_port.py +15 -0
- devdoctor-0.2.0/src/devdoctor/reporter.py +50 -0
- devdoctor-0.2.0/src/devdoctor/utils/system.py +30 -0
devdoctor-0.2.0/LICENSE
ADDED
|
@@ -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.
|
devdoctor-0.2.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/AttackBeaver/devdoctor/actions)
|
|
34
|
+
[](https://pypi.org/project/devdoctor/)
|
|
35
|
+
[](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
|
+
[](https://github.com/AttackBeaver/devdoctor/actions)
|
|
4
|
+
[](https://pypi.org/project/devdoctor/)
|
|
5
|
+
[](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,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,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
|