devmedic 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.
- devmedic-0.2.0/LICENSE +21 -0
- devmedic-0.2.0/PKG-INFO +133 -0
- devmedic-0.2.0/README.md +111 -0
- devmedic-0.2.0/devmedic/__init__.py +1 -0
- devmedic-0.2.0/devmedic/__main__.py +5 -0
- devmedic-0.2.0/devmedic/caches.py +173 -0
- devmedic-0.2.0/devmedic/cli.py +152 -0
- devmedic-0.2.0/devmedic/dashboard.py +645 -0
- devmedic-0.2.0/devmedic/junk.py +185 -0
- devmedic-0.2.0/devmedic/menu.py +295 -0
- devmedic-0.2.0/devmedic/ports.py +126 -0
- devmedic-0.2.0/devmedic/procs.py +113 -0
- devmedic-0.2.0/devmedic/repos.py +129 -0
- devmedic-0.2.0/devmedic/util.py +109 -0
- devmedic-0.2.0/devmedic.egg-info/PKG-INFO +133 -0
- devmedic-0.2.0/devmedic.egg-info/SOURCES.txt +21 -0
- devmedic-0.2.0/devmedic.egg-info/dependency_links.txt +1 -0
- devmedic-0.2.0/devmedic.egg-info/entry_points.txt +2 -0
- devmedic-0.2.0/devmedic.egg-info/requires.txt +2 -0
- devmedic-0.2.0/devmedic.egg-info/top_level.txt +1 -0
- devmedic-0.2.0/pyproject.toml +35 -0
- devmedic-0.2.0/setup.cfg +4 -0
- devmedic-0.2.0/tests/test_devmedic.py +219 -0
devmedic-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 devmedic contributors
|
|
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.
|
devmedic-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devmedic
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A health check for your developer machine: ports, junk folders, caches, git repos, dev processes — offline.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/yaswanthme007/devmedic
|
|
7
|
+
Project-URL: Issues, https://github.com/yaswanthme007/devmedic/issues
|
|
8
|
+
Keywords: developer-tools,cli,linux,ubuntu,cleanup,ports,disk-usage
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development
|
|
15
|
+
Classifier: Topic :: System :: Monitoring
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: psutil>=5.8
|
|
20
|
+
Requires-Dist: rich>=12.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# 🩺 devmedic
|
|
24
|
+
|
|
25
|
+
**A health check for your developer machine.** Find what's hogging your ports, RAM and disk,
|
|
26
|
+
spot git work that isn't backed up, and clean up safely — all from one friendly terminal UI.
|
|
27
|
+
Works **100% offline**. No accounts, no telemetry, nothing leaves your machine.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
_ _ _
|
|
31
|
+
__| | ___ __ __ _ __ ___ ___ __| |(_) ___
|
|
32
|
+
/ _` | / _ \\ \ / /| '_ ` _ \ / _ \ / _` || | / __|
|
|
33
|
+
| (_| || __/ \ V / | | | | | || __/| (_| || || (__
|
|
34
|
+
\__,_| \___| \_/ |_| |_| |_| \___| \__,_||_| \___| HEALTH 92/100 A
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Why?
|
|
38
|
+
|
|
39
|
+
Every developer hits these, over and over:
|
|
40
|
+
|
|
41
|
+
- 😤 `Error: listen EADDRINUSE: address already in use :::3000`
|
|
42
|
+
- 💾 Disk full — from 40 forgotten `node_modules`, `.venv`s and `target/` folders
|
|
43
|
+
- 🐢 Machine slow — three dev servers you started last week are still running
|
|
44
|
+
- 😱 "Did I ever push that project?"
|
|
45
|
+
|
|
46
|
+
devmedic answers all of them in seconds.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
Requires **Linux** (built and tested on Ubuntu) and **Python 3.9+**.
|
|
51
|
+
On **Windows**, use it inside [WSL](https://learn.microsoft.com/windows/wsl/install) for now; native support is planned.
|
|
52
|
+
|
|
53
|
+
**With pipx (recommended)**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
sudo apt install pipx && pipx ensurepath # once
|
|
57
|
+
pipx install devmedic
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**With pip**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install devmedic # inside a virtualenv, or: pip install --user devmedic
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**From a clone**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/yaswanthme007/devmedic.git
|
|
70
|
+
cd devmedic
|
|
71
|
+
./install.sh # uses pipx if present, otherwise a dev-mode link in ~/.local/bin
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Dev mode needs `sudo apt install python3-psutil python3-rich` (already present on most Ubuntu
|
|
75
|
+
desktops).
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
devmedic # interactive menu — pick a section with ↑↓ / 1-8 / Enter
|
|
81
|
+
devmedic all # everything on one screen (also used when output is piped)
|
|
82
|
+
devmedic watch # live full-screen monitor: CPU/RAM graphs, net speed, ports
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Sections** (open from the menu, or directly):
|
|
86
|
+
|
|
87
|
+
| Command | Shows |
|
|
88
|
+
|----------------------|---------------------------------------------------------------|
|
|
89
|
+
| `devmedic system` | CPU model, per-core usage, load, temperature, battery |
|
|
90
|
+
| `devmedic memory` | RAM/swap and the biggest apps (browser tabs grouped together) |
|
|
91
|
+
| `devmedic storage` | Usage of every real partition |
|
|
92
|
+
| `devmedic network` | IPs, Wi-Fi, traffic, listening ports |
|
|
93
|
+
| `devmedic tools` | Installed versions of git, python, node, go, rust, docker … |
|
|
94
|
+
| `devmedic projects` | Dev processes by project + every git repo's status |
|
|
95
|
+
| `devmedic cleanup` | How much space caches and junk folders are wasting |
|
|
96
|
+
| `devmedic notes` | The doctor's recommendations, with exact commands |
|
|
97
|
+
|
|
98
|
+
**Fix things:**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
devmedic ports [-a] # what's listening, and from which project folder
|
|
102
|
+
devmedic kill 3000 # free port 3000 (SIGTERM, then SIGKILL if needed)
|
|
103
|
+
devmedic junk [PATH] # node_modules, .venv, target/, .next … with sizes
|
|
104
|
+
devmedic junk --older 30 --clean # pick stale ones to delete: 1,3,5-8 / all
|
|
105
|
+
devmedic caches # pip npm yarn pnpm cargo go gradle maven … + system
|
|
106
|
+
devmedic caches --clean pip npm # or --clean all (skips risky ones like Trash)
|
|
107
|
+
devmedic repos [PATH] # uncommitted, unpushed, stashed, or no remote at all
|
|
108
|
+
devmedic procs [-v] # dev processes grouped by project, RAM/CPU/ports
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Safety
|
|
112
|
+
|
|
113
|
+
devmedic is careful by design:
|
|
114
|
+
|
|
115
|
+
- **Nothing is deleted without confirmation** (unless you pass `-y`).
|
|
116
|
+
- **Conservative junk detection:** `target/` only next to `Cargo.toml`/`pom.xml`, venvs only if
|
|
117
|
+
they contain `pyvenv.cfg`. Generic `build/` and `dist/` folders are **never** touched.
|
|
118
|
+
- **Never runs `sudo`.** System fixes (journal, apt, snap, docker) are printed for you to copy.
|
|
119
|
+
- **Only kills what you confirm**, and can't touch other users' processes.
|
|
120
|
+
- **Offline and private:** no network requests, no data collection.
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
./install.sh --dev # edits take effect immediately
|
|
126
|
+
python3 -m unittest discover -s tests -v # run the tests
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Ideas and pull requests are welcome! Please open an issue first for big changes.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
[MIT](LICENSE)
|
devmedic-0.2.0/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# 🩺 devmedic
|
|
2
|
+
|
|
3
|
+
**A health check for your developer machine.** Find what's hogging your ports, RAM and disk,
|
|
4
|
+
spot git work that isn't backed up, and clean up safely — all from one friendly terminal UI.
|
|
5
|
+
Works **100% offline**. No accounts, no telemetry, nothing leaves your machine.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
_ _ _
|
|
9
|
+
__| | ___ __ __ _ __ ___ ___ __| |(_) ___
|
|
10
|
+
/ _` | / _ \\ \ / /| '_ ` _ \ / _ \ / _` || | / __|
|
|
11
|
+
| (_| || __/ \ V / | | | | | || __/| (_| || || (__
|
|
12
|
+
\__,_| \___| \_/ |_| |_| |_| \___| \__,_||_| \___| HEALTH 92/100 A
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Why?
|
|
16
|
+
|
|
17
|
+
Every developer hits these, over and over:
|
|
18
|
+
|
|
19
|
+
- 😤 `Error: listen EADDRINUSE: address already in use :::3000`
|
|
20
|
+
- 💾 Disk full — from 40 forgotten `node_modules`, `.venv`s and `target/` folders
|
|
21
|
+
- 🐢 Machine slow — three dev servers you started last week are still running
|
|
22
|
+
- 😱 "Did I ever push that project?"
|
|
23
|
+
|
|
24
|
+
devmedic answers all of them in seconds.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Requires **Linux** (built and tested on Ubuntu) and **Python 3.9+**.
|
|
29
|
+
On **Windows**, use it inside [WSL](https://learn.microsoft.com/windows/wsl/install) for now; native support is planned.
|
|
30
|
+
|
|
31
|
+
**With pipx (recommended)**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
sudo apt install pipx && pipx ensurepath # once
|
|
35
|
+
pipx install devmedic
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**With pip**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install devmedic # inside a virtualenv, or: pip install --user devmedic
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**From a clone**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/yaswanthme007/devmedic.git
|
|
48
|
+
cd devmedic
|
|
49
|
+
./install.sh # uses pipx if present, otherwise a dev-mode link in ~/.local/bin
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Dev mode needs `sudo apt install python3-psutil python3-rich` (already present on most Ubuntu
|
|
53
|
+
desktops).
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
devmedic # interactive menu — pick a section with ↑↓ / 1-8 / Enter
|
|
59
|
+
devmedic all # everything on one screen (also used when output is piped)
|
|
60
|
+
devmedic watch # live full-screen monitor: CPU/RAM graphs, net speed, ports
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Sections** (open from the menu, or directly):
|
|
64
|
+
|
|
65
|
+
| Command | Shows |
|
|
66
|
+
|----------------------|---------------------------------------------------------------|
|
|
67
|
+
| `devmedic system` | CPU model, per-core usage, load, temperature, battery |
|
|
68
|
+
| `devmedic memory` | RAM/swap and the biggest apps (browser tabs grouped together) |
|
|
69
|
+
| `devmedic storage` | Usage of every real partition |
|
|
70
|
+
| `devmedic network` | IPs, Wi-Fi, traffic, listening ports |
|
|
71
|
+
| `devmedic tools` | Installed versions of git, python, node, go, rust, docker … |
|
|
72
|
+
| `devmedic projects` | Dev processes by project + every git repo's status |
|
|
73
|
+
| `devmedic cleanup` | How much space caches and junk folders are wasting |
|
|
74
|
+
| `devmedic notes` | The doctor's recommendations, with exact commands |
|
|
75
|
+
|
|
76
|
+
**Fix things:**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
devmedic ports [-a] # what's listening, and from which project folder
|
|
80
|
+
devmedic kill 3000 # free port 3000 (SIGTERM, then SIGKILL if needed)
|
|
81
|
+
devmedic junk [PATH] # node_modules, .venv, target/, .next … with sizes
|
|
82
|
+
devmedic junk --older 30 --clean # pick stale ones to delete: 1,3,5-8 / all
|
|
83
|
+
devmedic caches # pip npm yarn pnpm cargo go gradle maven … + system
|
|
84
|
+
devmedic caches --clean pip npm # or --clean all (skips risky ones like Trash)
|
|
85
|
+
devmedic repos [PATH] # uncommitted, unpushed, stashed, or no remote at all
|
|
86
|
+
devmedic procs [-v] # dev processes grouped by project, RAM/CPU/ports
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Safety
|
|
90
|
+
|
|
91
|
+
devmedic is careful by design:
|
|
92
|
+
|
|
93
|
+
- **Nothing is deleted without confirmation** (unless you pass `-y`).
|
|
94
|
+
- **Conservative junk detection:** `target/` only next to `Cargo.toml`/`pom.xml`, venvs only if
|
|
95
|
+
they contain `pyvenv.cfg`. Generic `build/` and `dist/` folders are **never** touched.
|
|
96
|
+
- **Never runs `sudo`.** System fixes (journal, apt, snap, docker) are printed for you to copy.
|
|
97
|
+
- **Only kills what you confirm**, and can't touch other users' processes.
|
|
98
|
+
- **Offline and private:** no network requests, no data collection.
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
./install.sh --dev # edits take effect immediately
|
|
104
|
+
python3 -m unittest discover -s tests -v # run the tests
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Ideas and pull requests are welcome! Please open an issue first for big changes.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Global tool caches (pip, npm, cargo, …) plus system things like journald/apt/docker."""
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from .util import (HOME, confirm, console, dir_size, have, human_size, remove_tree,
|
|
10
|
+
run, short_path, size_style)
|
|
11
|
+
|
|
12
|
+
h = lambda *p: os.path.join(HOME, *p)
|
|
13
|
+
|
|
14
|
+
# key, label, path, clean command (None -> delete dir contents), note
|
|
15
|
+
CACHES = [
|
|
16
|
+
("pip", "pip cache", h(".cache", "pip"), ["pip", "cache", "purge"], ""),
|
|
17
|
+
("uv", "uv cache", h(".cache", "uv"), ["uv", "cache", "clean"], ""),
|
|
18
|
+
("npm", "npm cache", h(".npm", "_cacache"), ["npm", "cache", "clean", "--force"], ""),
|
|
19
|
+
("yarn", "yarn cache", h(".cache", "yarn"), ["yarn", "cache", "clean"], ""),
|
|
20
|
+
("pnpm", "pnpm store", h(".local", "share", "pnpm", "store"), ["pnpm", "store", "prune"], "prunes unused only"),
|
|
21
|
+
("cargo", "cargo registry", h(".cargo", "registry"), None, ""),
|
|
22
|
+
("go", "Go module cache", h("go", "pkg", "mod"), ["go", "clean", "-modcache"], ""),
|
|
23
|
+
("gobuild", "Go build cache", h(".cache", "go-build"), ["go", "clean", "-cache"], ""),
|
|
24
|
+
("gradle", "Gradle caches", h(".gradle", "caches"), None, ""),
|
|
25
|
+
("maven", "Maven repo", h(".m2", "repository"), None, ""),
|
|
26
|
+
("hf", "HuggingFace models", h(".cache", "huggingface"), None, "re-download is big!"),
|
|
27
|
+
("thumbs", "Thumbnails", h(".cache", "thumbnails"), None, ""),
|
|
28
|
+
("trash", "Trash", h(".local", "share", "Trash"), None, "permanent!"),
|
|
29
|
+
("vscode", "VS Code cache", h(".config", "Code", "Cache"), None, "close VS Code first"),
|
|
30
|
+
("chrome", "Chrome cache", h(".cache", "google-chrome"), None, "close Chrome first"),
|
|
31
|
+
("firefox", "Firefox cache", h(".cache", "mozilla"), None, "close Firefox first"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def journal_usage():
|
|
36
|
+
out = run(["journalctl", "--disk-usage"])
|
|
37
|
+
m = re.search(r"take up ([\d.]+)([KMGT])", out)
|
|
38
|
+
if not m:
|
|
39
|
+
return None
|
|
40
|
+
mult = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4}[m.group(2)]
|
|
41
|
+
return int(float(m.group(1)) * mult)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def apt_usage():
|
|
45
|
+
path = "/var/cache/apt/archives"
|
|
46
|
+
return dir_size(path) if os.path.isdir(path) else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def snap_disabled():
|
|
50
|
+
"""Old disabled snap revisions: list of (name, rev, bytes)."""
|
|
51
|
+
out = run(["snap", "list", "--all"]) if have("snap") else ""
|
|
52
|
+
res = []
|
|
53
|
+
for line in out.splitlines()[1:]:
|
|
54
|
+
cols = line.split()
|
|
55
|
+
if len(cols) >= 3 and "disabled" in line:
|
|
56
|
+
snap_file = f"/var/lib/snapd/snaps/{cols[0]}_{cols[2]}.snap"
|
|
57
|
+
try:
|
|
58
|
+
size = os.stat(snap_file).st_blocks * 512
|
|
59
|
+
except OSError:
|
|
60
|
+
size = 0
|
|
61
|
+
res.append((cols[0], cols[2], size))
|
|
62
|
+
return res
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def docker_usage():
|
|
66
|
+
"""Reclaimable bytes from `docker system df`, or None if unavailable."""
|
|
67
|
+
if not have("docker"):
|
|
68
|
+
return None
|
|
69
|
+
out = run(["docker", "system", "df", "--format", "{{.Reclaimable}}"], timeout=20)
|
|
70
|
+
if not out.strip():
|
|
71
|
+
return None
|
|
72
|
+
total = 0
|
|
73
|
+
for line in out.splitlines():
|
|
74
|
+
m = re.match(r"([\d.]+)\s*([kKMGT]?B)", line.strip())
|
|
75
|
+
if m:
|
|
76
|
+
mult = {"B": 1, "kB": 1000, "KB": 1000, "MB": 1000**2,
|
|
77
|
+
"GB": 1000**3, "TB": 1000**4}[m.group(2)]
|
|
78
|
+
total += int(float(m.group(1)) * mult)
|
|
79
|
+
return total
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def collect():
|
|
83
|
+
present = [c for c in CACHES if os.path.isdir(c[2])]
|
|
84
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
85
|
+
sizes = list(pool.map(dir_size, [c[2] for c in present]))
|
|
86
|
+
j, a, d = pool.submit(journal_usage), pool.submit(apt_usage), pool.submit(docker_usage)
|
|
87
|
+
snaps = pool.submit(snap_disabled)
|
|
88
|
+
user = [{"key": c[0], "label": c[1], "path": c[2], "cmd": c[3], "note": c[4],
|
|
89
|
+
"size": s} for c, s in zip(present, sizes) if s > 0]
|
|
90
|
+
user.sort(key=lambda r: r["size"], reverse=True)
|
|
91
|
+
system = []
|
|
92
|
+
if j.result() and j.result() > 250 * 1024**2:
|
|
93
|
+
system.append({"key": "journal", "label": "systemd journal logs", "size": j.result(),
|
|
94
|
+
"fix": "sudo journalctl --vacuum-size=200M"})
|
|
95
|
+
if a.result():
|
|
96
|
+
system.append({"key": "apt", "label": "apt package cache", "size": a.result(),
|
|
97
|
+
"fix": "sudo apt clean && sudo apt autoremove --purge"})
|
|
98
|
+
if d.result():
|
|
99
|
+
system.append({"key": "docker", "label": "Docker (reclaimable)", "size": d.result(),
|
|
100
|
+
"fix": "docker system prune (add -a --volumes for everything)"})
|
|
101
|
+
if snaps.result():
|
|
102
|
+
cmds = "; ".join(f"sudo snap remove {n} --revision={r}" for n, r, _ in snaps.result())
|
|
103
|
+
system.append({"key": "snap", "label": f"Old snap revisions ({len(snaps.result())})",
|
|
104
|
+
"size": sum(sz for *_, sz in snaps.result()) or None, "fix": cmds})
|
|
105
|
+
return user, system
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def show(user, system):
|
|
109
|
+
t = Table(title="Caches you can clean", title_justify="left")
|
|
110
|
+
t.add_column("Key", style="bold cyan")
|
|
111
|
+
t.add_column("Size", justify="right")
|
|
112
|
+
t.add_column("What")
|
|
113
|
+
t.add_column("Path", style="dim")
|
|
114
|
+
t.add_column("Note", style="yellow")
|
|
115
|
+
for r in user:
|
|
116
|
+
t.add_row(r["key"], f"[{size_style(r['size'])}]{human_size(r['size'])}[/]",
|
|
117
|
+
r["label"], short_path(r["path"]), r["note"])
|
|
118
|
+
console.print(t)
|
|
119
|
+
total = sum(r["size"] for r in user)
|
|
120
|
+
console.print(f"[bold]{human_size(total)}[/] in user caches. "
|
|
121
|
+
f"Clean with: [bold]devmedic caches --clean pip npm …[/] or [bold]--clean all[/]")
|
|
122
|
+
if system:
|
|
123
|
+
s = Table(title="System (needs sudo / docker)", title_justify="left")
|
|
124
|
+
s.add_column("Size", justify="right")
|
|
125
|
+
s.add_column("What")
|
|
126
|
+
s.add_column("Fix command", style="bold", overflow="fold")
|
|
127
|
+
for r in system:
|
|
128
|
+
size = f"[{size_style(r['size'])}]{human_size(r['size'])}[/]" if r["size"] else "—"
|
|
129
|
+
s.add_row(size, r["label"], r["fix"])
|
|
130
|
+
console.print(s)
|
|
131
|
+
console.print("[dim]devmedic never runs sudo itself — copy the command if you want it.[/]")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def clean(user, keys, assume_yes=False):
|
|
135
|
+
by_key = {r["key"]: r for r in user}
|
|
136
|
+
if "all" in keys:
|
|
137
|
+
# 'all' deliberately excludes the ones with a warning note.
|
|
138
|
+
chosen = [r for r in user if not r["note"]]
|
|
139
|
+
else:
|
|
140
|
+
unknown = [k for k in keys if k not in by_key]
|
|
141
|
+
if unknown:
|
|
142
|
+
console.print(f"[red]Unknown / empty cache(s): {', '.join(unknown)}[/]. "
|
|
143
|
+
f"Available: {', '.join(by_key) or 'none'}")
|
|
144
|
+
chosen = [by_key[k] for k in keys if k in by_key]
|
|
145
|
+
if not chosen:
|
|
146
|
+
return
|
|
147
|
+
total = sum(r["size"] for r in chosen)
|
|
148
|
+
for r in chosen:
|
|
149
|
+
console.print(f" • {r['label']:<22} {human_size(r['size']):>10}"
|
|
150
|
+
+ (f" [yellow]({r['note']})[/]" if r["note"] else ""))
|
|
151
|
+
if not confirm(f"Clean these ({human_size(total)})?", assume_yes):
|
|
152
|
+
return
|
|
153
|
+
for r in chosen:
|
|
154
|
+
ok = False
|
|
155
|
+
if r["cmd"] and have(r["cmd"][0]):
|
|
156
|
+
try:
|
|
157
|
+
ok = subprocess.run(r["cmd"], capture_output=True, timeout=300).returncode == 0
|
|
158
|
+
except (OSError, subprocess.SubprocessError):
|
|
159
|
+
ok = False
|
|
160
|
+
if not ok:
|
|
161
|
+
ok = all(remove_tree(e.path) if e.is_dir(follow_symlinks=False)
|
|
162
|
+
else _unlink(e.path) for e in os.scandir(r["path"]))
|
|
163
|
+
after = dir_size(r["path"]) if os.path.isdir(r["path"]) else 0
|
|
164
|
+
mark = "[green]✔[/]" if ok else "[yellow]~[/]"
|
|
165
|
+
console.print(f"{mark} {r['label']}: freed {human_size(max(r['size'] - after, 0))}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _unlink(path):
|
|
169
|
+
try:
|
|
170
|
+
os.unlink(path)
|
|
171
|
+
return True
|
|
172
|
+
except OSError:
|
|
173
|
+
return False
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""devmedic — a health check for your developer machine."""
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from . import __version__, caches, dashboard, junk, menu, ports, procs, repos
|
|
7
|
+
from .util import console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cmd_overview(args):
|
|
11
|
+
# Interactive menu in a real terminal; full dashboard when piped/redirected.
|
|
12
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
13
|
+
menu.run()
|
|
14
|
+
else:
|
|
15
|
+
dashboard.show()
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cmd_all(args):
|
|
20
|
+
dashboard.show()
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cmd_section(args):
|
|
25
|
+
menu.show_section(args.cmd)
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cmd_watch(args):
|
|
30
|
+
dashboard.watch(args.interval)
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cmd_ports(args):
|
|
35
|
+
ports.show(args.all)
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_kill(args):
|
|
40
|
+
return ports.kill(args.port, args.force, args.yes)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_junk(args):
|
|
44
|
+
items = junk.scan(args.path, args.older)
|
|
45
|
+
junk.show(items, args.limit, args.path)
|
|
46
|
+
if args.clean:
|
|
47
|
+
junk.clean(items, args.yes, args.limit)
|
|
48
|
+
elif items:
|
|
49
|
+
console.print("[dim]Run again with --clean to pick folders to delete. "
|
|
50
|
+
"--older 30 shows only projects untouched for 30 days.[/]")
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_caches(args):
|
|
55
|
+
user, system = caches.collect()
|
|
56
|
+
caches.show(user, system)
|
|
57
|
+
if args.clean:
|
|
58
|
+
console.print()
|
|
59
|
+
caches.clean(user, args.clean, args.yes)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_repos(args):
|
|
64
|
+
repos.show(repos.scan(args.path), args.all, args.path)
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_procs(args):
|
|
69
|
+
procs.show(procs.collect(), args.verbose)
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_parser():
|
|
74
|
+
p = argparse.ArgumentParser(prog="devmedic", description=__doc__)
|
|
75
|
+
p.add_argument("--version", action="version", version=f"devmedic {__version__}")
|
|
76
|
+
sub = p.add_subparsers(dest="cmd")
|
|
77
|
+
|
|
78
|
+
s = sub.add_parser("menu", help="interactive section picker (default)")
|
|
79
|
+
s.set_defaults(func=cmd_overview)
|
|
80
|
+
|
|
81
|
+
s = sub.add_parser("all", aliases=["check"], help="full dashboard on one screen")
|
|
82
|
+
s.set_defaults(func=cmd_all)
|
|
83
|
+
|
|
84
|
+
for key, icon, title, *_ in menu.SECTIONS:
|
|
85
|
+
s = sub.add_parser(key, help=f"{title} section")
|
|
86
|
+
s.set_defaults(func=cmd_section)
|
|
87
|
+
|
|
88
|
+
s = sub.add_parser("watch", help="live full-screen dashboard (Ctrl+C to exit)")
|
|
89
|
+
s.add_argument("-n", "--interval", type=float, default=1.0, metavar="SEC")
|
|
90
|
+
s.set_defaults(func=cmd_watch)
|
|
91
|
+
|
|
92
|
+
s = sub.add_parser("ports", help="what is listening on which port")
|
|
93
|
+
s.add_argument("-a", "--all", action="store_true", help="include system services")
|
|
94
|
+
s.set_defaults(func=cmd_ports)
|
|
95
|
+
|
|
96
|
+
s = sub.add_parser("kill", help="kill whatever holds a port")
|
|
97
|
+
s.add_argument("port", type=int)
|
|
98
|
+
s.add_argument("-f", "--force", action="store_true", help="SIGKILL immediately")
|
|
99
|
+
s.add_argument("-y", "--yes", action="store_true", help="don't ask")
|
|
100
|
+
s.set_defaults(func=cmd_kill)
|
|
101
|
+
|
|
102
|
+
s = sub.add_parser("junk", help="find node_modules, .venv, target/ … and their sizes")
|
|
103
|
+
s.add_argument("path", nargs="?", default=os.path.expanduser("~"))
|
|
104
|
+
s.add_argument("--older", type=int, default=0, metavar="DAYS",
|
|
105
|
+
help="only projects untouched for DAYS days")
|
|
106
|
+
s.add_argument("--limit", type=int, default=40)
|
|
107
|
+
s.add_argument("--clean", action="store_true", help="choose folders to delete")
|
|
108
|
+
s.add_argument("-y", "--yes", action="store_true", help="delete all listed without asking")
|
|
109
|
+
s.set_defaults(func=cmd_junk)
|
|
110
|
+
|
|
111
|
+
s = sub.add_parser("caches", help="tool & system caches (pip, npm, docker, journal …)")
|
|
112
|
+
s.add_argument("--clean", nargs="+", metavar="KEY", help="cache keys to clean, or 'all'")
|
|
113
|
+
s.add_argument("-y", "--yes", action="store_true")
|
|
114
|
+
s.set_defaults(func=cmd_caches)
|
|
115
|
+
|
|
116
|
+
s = sub.add_parser("repos", help="git repos with uncommitted / unpushed work")
|
|
117
|
+
s.add_argument("path", nargs="?", default=os.path.expanduser("~"))
|
|
118
|
+
s.add_argument("-a", "--all", action="store_true", help="show clean repos too")
|
|
119
|
+
s.set_defaults(func=cmd_repos)
|
|
120
|
+
|
|
121
|
+
s = sub.add_parser("procs", help="dev processes grouped by project")
|
|
122
|
+
s.add_argument("-v", "--verbose", action="store_true")
|
|
123
|
+
s.set_defaults(func=cmd_procs)
|
|
124
|
+
return p
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
WSL_URL = "https://learn.microsoft.com/windows/wsl/install"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main(argv=None):
|
|
131
|
+
args = build_parser().parse_args(argv)
|
|
132
|
+
if sys.platform == "win32":
|
|
133
|
+
console.print("[bold yellow]devmedic doesn't support native Windows yet.[/]\n"
|
|
134
|
+
"It works great inside WSL (Ubuntu on Windows):\n"
|
|
135
|
+
f" 1. Install WSL: [cyan]{WSL_URL}[/]\n"
|
|
136
|
+
" 2. In the Ubuntu terminal: [bold]pipx install devmedic[/]\n"
|
|
137
|
+
"Native Windows support is coming in a future release.")
|
|
138
|
+
return 1
|
|
139
|
+
if not sys.platform.startswith("linux"):
|
|
140
|
+
console.print("[yellow]devmedic is built for Linux; some checks may be "
|
|
141
|
+
"missing on this system.[/]")
|
|
142
|
+
if not getattr(args, "func", None):
|
|
143
|
+
args.func = cmd_overview
|
|
144
|
+
try:
|
|
145
|
+
return args.func(args) or 0
|
|
146
|
+
except KeyboardInterrupt:
|
|
147
|
+
console.print("\n[dim]Interrupted.[/]")
|
|
148
|
+
return 130
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
sys.exit(main())
|