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 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.
@@ -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)
@@ -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,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -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())