devmedic 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
devmedic/procs.py ADDED
@@ -0,0 +1,113 @@
1
+ """Dev processes grouped by the project (cwd) they were started from."""
2
+ import os
3
+ import time
4
+ from collections import defaultdict
5
+
6
+ import psutil
7
+ from rich.table import Table
8
+
9
+ from .util import HOME, console, human_age, human_size, short_path, size_style
10
+
11
+ DEV_NAMES = {"node", "python", "python3", "java", "ruby", "go", "cargo", "rustc",
12
+ "deno", "bun", "php", "dotnet", "gradle", "mvn", "npm", "pnpm", "yarn",
13
+ "vite", "esbuild", "webpack", "next-server", "uvicorn", "gunicorn",
14
+ "flask", "celery", "jupyter", "jupyter-lab", "postgres", "mysqld",
15
+ "mongod", "redis-server", "tsserver", "ng", "air", "nodemon",
16
+ "docker-proxy", "containerd-shim", "gopls", "rust-analyzer", "pyright",
17
+ "language_server", "kotlin", "emulator", "qemu-system-x86_64", "adb"}
18
+
19
+
20
+ def is_dev(name, cmdline):
21
+ base = name.split(".")[0].lower()
22
+ if base in DEV_NAMES or name.lower() in DEV_NAMES:
23
+ return True
24
+ return bool(cmdline) and os.path.basename(cmdline[0]).lower() in DEV_NAMES
25
+
26
+
27
+ def collect():
28
+ ports = defaultdict(set)
29
+ try:
30
+ for c in psutil.net_connections(kind="inet"):
31
+ if c.status == psutil.CONN_LISTEN and c.pid:
32
+ ports[c.pid].add(c.laddr.port)
33
+ except (psutil.AccessDenied, OSError):
34
+ pass
35
+ me, self_pid = os.getuid(), os.getpid()
36
+ rows = []
37
+ for p in psutil.process_iter(["pid", "name", "cmdline", "memory_info", "create_time",
38
+ "uids", "cwd", "cpu_percent"]):
39
+ info = p.info
40
+ if not info["uids"] or info["uids"].real != me or info["pid"] == self_pid:
41
+ continue
42
+ if not is_dev(info["name"] or "", info["cmdline"] or []):
43
+ continue
44
+ rows.append({"pid": info["pid"], "name": info["name"],
45
+ "cmd": " ".join(" ".join(info["cmdline"] or []).split()),
46
+ "rss": info["memory_info"].rss if info["memory_info"] else 0,
47
+ "started": info["create_time"] or time.time(),
48
+ "cwd": info["cwd"] or "?", "ports": sorted(ports.get(info["pid"], ())),
49
+ "proc": p})
50
+ # One CPU sample over a short window for everything at once.
51
+ for r in rows:
52
+ try:
53
+ r["proc"].cpu_percent(None)
54
+ except psutil.Error:
55
+ pass
56
+ time.sleep(0.5)
57
+ for r in rows:
58
+ try:
59
+ r["cpu"] = r["proc"].cpu_percent(None)
60
+ except psutil.Error:
61
+ r["cpu"] = 0.0
62
+ return rows
63
+
64
+
65
+ def group(rows):
66
+ groups = defaultdict(list)
67
+ for r in rows:
68
+ groups[r["cwd"]].append(r)
69
+ out = [{"cwd": cwd, "procs": ps, "rss": sum(p["rss"] for p in ps),
70
+ "cpu": sum(p["cpu"] for p in ps),
71
+ "ports": sorted({x for p in ps for x in p["ports"]}),
72
+ "oldest": min(p["started"] for p in ps)}
73
+ for cwd, ps in groups.items()]
74
+ return sorted(out, key=lambda g: g["rss"], reverse=True)
75
+
76
+
77
+ def show(rows, detail=False):
78
+ if not rows:
79
+ console.print("[green]No dev processes running.[/]")
80
+ return
81
+ vm = psutil.virtual_memory()
82
+ groups = group(rows)
83
+ total = sum(g["rss"] for g in groups)
84
+ t = Table(title=f"Dev processes — {human_size(total)} RAM "
85
+ f"({total / vm.total * 100:.0f}% of {human_size(vm.total)})",
86
+ title_justify="left")
87
+ t.add_column("Started in", overflow="fold")
88
+ t.add_column("RAM", justify="right")
89
+ t.add_column("CPU", justify="right")
90
+ t.add_column("Procs", justify="right")
91
+ t.add_column("Ports", style="cyan")
92
+ t.add_column("Running for", justify="right")
93
+ t.add_column("What", overflow="ellipsis", no_wrap=True, max_width=50)
94
+ for g in groups:
95
+ names = sorted({p["name"] for p in g["procs"]})
96
+ cwd = g["cwd"]
97
+ label = "~" if cwd == HOME else short_path(cwd)
98
+ t.add_row(label, f"[{size_style(g['rss'] * 4)}]{human_size(g['rss'])}[/]",
99
+ f"{g['cpu']:.0f}%", str(len(g["procs"])),
100
+ ", ".join(map(str, g["ports"])),
101
+ human_age(g["oldest"]).replace(" ago", ""), ", ".join(names))
102
+ if detail:
103
+ for p in sorted(g["procs"], key=lambda p: p["rss"], reverse=True):
104
+ t.add_row(f" [dim]pid {p['pid']}[/]", f"[dim]{human_size(p['rss'])}[/]",
105
+ f"[dim]{p['cpu']:.0f}%[/]", "",
106
+ ", ".join(map(str, p["ports"])), "", f"[dim]{p['cmd']}[/]")
107
+ console.print(t)
108
+ stale = [g for g in groups if time.time() - g["oldest"] > 86400 and g["ports"]]
109
+ if stale:
110
+ console.print(f"[yellow]💡 {len(stale)} server(s) running for over a day — "
111
+ f"forgotten dev servers? Kill with [bold]devmedic kill <port>[/].[/]")
112
+ if not detail:
113
+ console.print("[dim]Use -v to see individual processes.[/]")
devmedic/repos.py ADDED
@@ -0,0 +1,129 @@
1
+ """Find git repos with work that isn't safely pushed."""
2
+ import os
3
+ import subprocess
4
+ from concurrent.futures import ThreadPoolExecutor
5
+
6
+ from rich.progress import Progress, SpinnerColumn, TextColumn
7
+ from rich.table import Table
8
+
9
+ from .junk import PATTERNS, SKIP_AT_TOP, SKIP_DIRS
10
+ from .util import HOME, console, display_path, human_age, short_path
11
+
12
+
13
+ def find(root, max_depth=6):
14
+ root = os.path.abspath(root)
15
+ stack = [(root, 0)]
16
+ while stack:
17
+ current, depth = stack.pop()
18
+ if os.path.exists(os.path.join(current, ".git")):
19
+ yield current
20
+ # keep going: nested repos / submodules-as-dirs are common
21
+ try:
22
+ entries = list(os.scandir(current))
23
+ except OSError:
24
+ continue
25
+ at_top = current in (HOME, "/")
26
+ for e in entries:
27
+ if (depth < max_depth and e.is_dir(follow_symlinks=False)
28
+ and e.name not in SKIP_DIRS and e.name not in PATTERNS
29
+ and not (at_top and (e.name.startswith(".") or e.name in SKIP_AT_TOP))):
30
+ stack.append((e.path, depth + 1))
31
+
32
+
33
+ def git(repo, *args):
34
+ try:
35
+ r = subprocess.run(["git", "-C", repo, *args], capture_output=True, text=True,
36
+ timeout=15)
37
+ return r.stdout if r.returncode == 0 else ""
38
+ except (OSError, subprocess.SubprocessError):
39
+ return ""
40
+
41
+
42
+ def inspect(repo):
43
+ info = {"path": repo, "branch": "?", "ahead": 0, "behind": 0, "upstream": False,
44
+ "changed": 0, "untracked": 0, "stashes": 0, "last": 0, "no_remote": False,
45
+ "unpushed_branches": 0}
46
+ for line in git(repo, "status", "--porcelain=v2", "--branch").splitlines():
47
+ if line.startswith("# branch.head "):
48
+ info["branch"] = line.split(" ", 2)[2]
49
+ elif line.startswith("# branch.upstream "):
50
+ info["upstream"] = True
51
+ elif line.startswith("# branch.ab "):
52
+ a, b = line.split()[2:4]
53
+ info["ahead"], info["behind"] = int(a), -int(b)
54
+ elif line.startswith("?"):
55
+ info["untracked"] += 1
56
+ elif line[:1] in ("1", "2", "u"):
57
+ info["changed"] += 1
58
+ info["stashes"] = len(git(repo, "stash", "list").splitlines())
59
+ last = git(repo, "log", "-1", "--format=%ct").strip()
60
+ info["last"] = int(last) if last.isdigit() else 0
61
+ info["no_remote"] = not git(repo, "remote").strip()
62
+ # Local branches with commits not on any remote.
63
+ if not info["no_remote"]:
64
+ out = git(repo, "for-each-ref", "--format=%(refname:short)|%(upstream:track)",
65
+ "refs/heads")
66
+ for line in out.splitlines():
67
+ name, _, track = line.partition("|")
68
+ if name == info["branch"]:
69
+ continue # already covered by ahead/upstream
70
+ if "ahead" in track or (not track and
71
+ git(repo, "log", "--oneline", "-1", name, "--not",
72
+ "--remotes").strip()):
73
+ info["unpushed_branches"] += 1
74
+ return info
75
+
76
+
77
+ def needs_attention(i):
78
+ return (i["changed"] or i["untracked"] or i["ahead"] or i["stashes"]
79
+ or i["unpushed_branches"] or i["no_remote"]
80
+ or (i["upstream"] is False and i["branch"] != "(detached)" and i["last"]))
81
+
82
+
83
+ def scan(root):
84
+ with Progress(SpinnerColumn(), TextColumn("{task.description}"),
85
+ console=console, transient=True) as prog:
86
+ task = prog.add_task(f"Looking for git repos in {short_path(os.path.abspath(root))} …")
87
+ repos = list(find(root))
88
+ prog.update(task, description=f"Inspecting {len(repos)} repos …")
89
+ with ThreadPoolExecutor(max_workers=8) as pool:
90
+ infos = list(pool.map(inspect, repos))
91
+ return sorted(infos, key=lambda i: i["last"], reverse=True)
92
+
93
+
94
+ def show(infos, show_all=False, root=None):
95
+ if not infos:
96
+ console.print("[dim]No git repositories found.[/]")
97
+ return
98
+ flagged = [i for i in infos if needs_attention(i)]
99
+ rows = infos if show_all else flagged
100
+ if not rows:
101
+ console.print(f"[green]All {len(infos)} repos are clean and pushed. ✨[/]")
102
+ return
103
+ t = Table(title=f"Git repos — {len(flagged)} of {len(infos)} need attention",
104
+ title_justify="left")
105
+ t.add_column("Repo", overflow="fold")
106
+ t.add_column("Branch", style="cyan")
107
+ t.add_column("Problems")
108
+ t.add_column("Last commit", justify="right")
109
+ for i in rows:
110
+ p = []
111
+ if i["changed"]:
112
+ p.append(f"[yellow]{i['changed']} modified[/]")
113
+ if i["untracked"]:
114
+ p.append(f"[yellow]{i['untracked']} untracked[/]")
115
+ if i["ahead"]:
116
+ p.append(f"[red]{i['ahead']} unpushed[/]")
117
+ if i["behind"]:
118
+ p.append(f"[blue]{i['behind']} behind[/]")
119
+ if i["stashes"]:
120
+ p.append(f"[magenta]{i['stashes']} stash[/]")
121
+ if i["unpushed_branches"]:
122
+ p.append(f"[red]{i['unpushed_branches']} other branch(es) unpushed[/]")
123
+ if i["no_remote"]:
124
+ p.append("[red]no remote (not backed up!)[/]")
125
+ elif not i["upstream"] and i["last"]:
126
+ p.append("[yellow]branch has no upstream[/]")
127
+ t.add_row(display_path(i["path"], root), i["branch"], ", ".join(p) or "[green]clean[/]",
128
+ human_age(i["last"]) if i["last"] else "no commits")
129
+ console.print(t)
devmedic/util.py ADDED
@@ -0,0 +1,109 @@
1
+ """Shared helpers: sizes, ages, confirmation prompts."""
2
+ import os
3
+ import shutil
4
+ import subprocess
5
+ import time
6
+
7
+ from rich.console import Console
8
+
9
+ console = Console()
10
+ HOME = os.path.expanduser("~")
11
+
12
+
13
+ def dir_size(path):
14
+ """Actual disk usage of a directory tree in bytes (doesn't follow symlinks)."""
15
+ total = 0
16
+ seen = set()
17
+ stack = [path]
18
+ while stack:
19
+ current = stack.pop()
20
+ try:
21
+ with os.scandir(current) as it:
22
+ for entry in it:
23
+ try:
24
+ st = entry.stat(follow_symlinks=False)
25
+ except OSError:
26
+ continue
27
+ if entry.is_dir(follow_symlinks=False):
28
+ stack.append(entry.path)
29
+ continue
30
+ # Hardlinked files (pnpm store, etc.) count once.
31
+ if st.st_nlink > 1:
32
+ key = (st.st_dev, st.st_ino)
33
+ if key in seen:
34
+ continue
35
+ seen.add(key)
36
+ total += st.st_blocks * 512
37
+ except OSError:
38
+ continue
39
+ return total
40
+
41
+
42
+ def human_size(n):
43
+ for unit in ("B", "KB", "MB", "GB", "TB"):
44
+ if abs(n) < 1024 or unit == "TB":
45
+ return f"{n:.0f} {unit}" if unit in ("B", "KB") else f"{n:.1f} {unit}"
46
+ n /= 1024
47
+
48
+
49
+ def size_style(n):
50
+ if n >= 1024**3:
51
+ return "bold red"
52
+ if n >= 200 * 1024**2:
53
+ return "yellow"
54
+ return "green"
55
+
56
+
57
+ def human_age(ts):
58
+ days = (time.time() - ts) / 86400
59
+ if days < 1:
60
+ hours = days * 24
61
+ return "just now" if hours < 1 else f"{hours:.0f}h ago"
62
+ if days < 60:
63
+ return f"{days:.0f}d ago"
64
+ if days < 730:
65
+ return f"{days / 30:.0f}mo ago"
66
+ return f"{days / 365:.1f}y ago"
67
+
68
+
69
+ def short_path(path):
70
+ return "~" + path[len(HOME):] if path.startswith(HOME) else path
71
+
72
+
73
+ def display_path(path, root=None):
74
+ """Path relative to the scan root (when that's shorter), else ~-style."""
75
+ if root:
76
+ root = os.path.abspath(root)
77
+ if root != HOME and path.startswith(root + os.sep):
78
+ return os.path.relpath(path, root)
79
+ return short_path(path)
80
+
81
+
82
+ def confirm(question, assume_yes=False):
83
+ if assume_yes:
84
+ return True
85
+ try:
86
+ answer = console.input(f"[bold]{question}[/] [dim](y/N)[/] ")
87
+ except (EOFError, KeyboardInterrupt):
88
+ console.print()
89
+ return False
90
+ return answer.strip().lower() in ("y", "yes")
91
+
92
+
93
+ def run(cmd, timeout=15):
94
+ """Run a command, return stdout ('' on any failure)."""
95
+ try:
96
+ return subprocess.run(
97
+ cmd, capture_output=True, text=True, timeout=timeout
98
+ ).stdout
99
+ except (OSError, subprocess.SubprocessError):
100
+ return ""
101
+
102
+
103
+ def have(binary):
104
+ return shutil.which(binary) is not None
105
+
106
+
107
+ def remove_tree(path):
108
+ shutil.rmtree(path, ignore_errors=True)
109
+ return not os.path.exists(path)
@@ -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,17 @@
1
+ devmedic/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
2
+ devmedic/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
3
+ devmedic/caches.py,sha256=b3PGwVXITq1WJtlbvuLyhZhoLjm-jAZDCvKo-hN1b0A,7917
4
+ devmedic/cli.py,sha256=mHtE2vLVnvVsYN0pVNkAAtpeqyiM9MFX34QqieGEdOI,5098
5
+ devmedic/dashboard.py,sha256=MvLipL3QEHPyPTq1IBttcfR0LcvYoNnIH6HLGqIXtz0,25474
6
+ devmedic/junk.py,sha256=KWNVYPADHIzjuutz2ULeKFQAbDW1V4Bm1Tp3xktOjSY,7339
7
+ devmedic/menu.py,sha256=9Lu--X7SqzSjlSjxbaAlpYRq0Unt9Tk_i0B5lYTREjY,10906
8
+ devmedic/ports.py,sha256=rYxQjfACEG-YMCt0S5HQZ9KSTP4ia9FMr_ALnZyp8Bw,4637
9
+ devmedic/procs.py,sha256=PrSsIVW8j6UETvkc4FV1D_iHNKi50faF9Ip8CN61BCY,4860
10
+ devmedic/repos.py,sha256=8ekx-1ibscyyG0zjdquz3EyDiZxvL4P2UaM6HWt5J8k,5403
11
+ devmedic/util.py,sha256=NiEX1xxm3JJgF0NX43nygZEmdLD7MEaw3K45wQma8oA,2977
12
+ devmedic-0.2.0.dist-info/licenses/LICENSE,sha256=64qGbQIfOrXIorKNN0D5R4FacU3r3JcYMfp9R1mK83Q,1078
13
+ devmedic-0.2.0.dist-info/METADATA,sha256=34tipsbC8d0FRbBARU1BzNBgEuTRiKO2L22RYWsCYKY,5275
14
+ devmedic-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
15
+ devmedic-0.2.0.dist-info/entry_points.txt,sha256=e0NQcvPkfQo1HsNkpje7CpQiTqh0Z6RT-kw2AafF9hg,47
16
+ devmedic-0.2.0.dist-info/top_level.txt,sha256=ND2dM3AMx6h4OsMSqXTvHWfrlJp5hX5kOW81K0AqTZI,9
17
+ devmedic-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devmedic = devmedic.cli:main
@@ -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 @@
1
+ devmedic