killpy 0.20.0__tar.gz → 0.22.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.
Files changed (43) hide show
  1. {killpy-0.20.0 → killpy-0.22.0}/PKG-INFO +131 -7
  2. {killpy-0.20.0 → killpy-0.22.0}/README.md +130 -6
  3. {killpy-0.20.0 → killpy-0.22.0}/killpy/__main__.py +11 -0
  4. {killpy-0.20.0 → killpy-0.22.0}/killpy/cli.py +52 -5
  5. killpy-0.22.0/killpy/commands/_utils.py +65 -0
  6. {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/clean.py +2 -4
  7. {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/delete.py +9 -24
  8. killpy-0.22.0/killpy/commands/doctor.py +285 -0
  9. {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/list.py +3 -25
  10. {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/stats.py +38 -1
  11. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/artifacts.py +2 -2
  12. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/conda.py +4 -4
  13. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/hatch.py +2 -2
  14. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/pipenv.py +2 -2
  15. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/pipx.py +32 -26
  16. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/poetry.py +2 -2
  17. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/pyenv.py +2 -2
  18. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/tox.py +2 -2
  19. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/uv.py +2 -2
  20. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/venv.py +0 -1
  21. killpy-0.22.0/killpy/intelligence/__init__.py +56 -0
  22. killpy-0.22.0/killpy/intelligence/git_analyzer.py +119 -0
  23. killpy-0.22.0/killpy/intelligence/scoring.py +227 -0
  24. killpy-0.22.0/killpy/intelligence/suggestions.py +120 -0
  25. killpy-0.22.0/killpy/intelligence/tracker.py +123 -0
  26. {killpy-0.20.0 → killpy-0.22.0}/killpy/models.py +75 -1
  27. {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/PKG-INFO +131 -7
  28. {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/SOURCES.txt +8 -1
  29. {killpy-0.20.0 → killpy-0.22.0}/pyproject.toml +11 -3
  30. {killpy-0.20.0 → killpy-0.22.0}/killpy/__init__.py +0 -0
  31. {killpy-0.20.0 → killpy-0.22.0}/killpy/cleaner.py +0 -0
  32. {killpy-0.20.0 → killpy-0.22.0}/killpy/cleaners/__init__.py +0 -0
  33. {killpy-0.20.0 → killpy-0.22.0}/killpy/commands/__init__.py +0 -0
  34. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/__init__.py +0 -0
  35. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/base.py +0 -0
  36. {killpy-0.20.0 → killpy-0.22.0}/killpy/detectors/cache.py +0 -0
  37. {killpy-0.20.0 → killpy-0.22.0}/killpy/files/__init__.py +0 -0
  38. {killpy-0.20.0 → killpy-0.22.0}/killpy/scanner.py +0 -0
  39. {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/dependency_links.txt +0 -0
  40. {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/entry_points.txt +0 -0
  41. {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/requires.txt +0 -0
  42. {killpy-0.20.0 → killpy-0.22.0}/killpy.egg-info/top_level.txt +0 -0
  43. {killpy-0.20.0 → killpy-0.22.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: killpy
3
- Version: 0.20.0
3
+ Version: 0.22.0
4
4
  Summary: List all .venv directories and Conda environments 🐍 on your system and check how much space they are using. You can then choose which ones to delete in order to free up space 🧹.
5
5
  Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
6
6
  Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
@@ -22,9 +22,13 @@ Requires-Dist: textual>=1.0.0
22
22
 
23
23
  ![Logo](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/logo.png)
24
24
 
25
- ### Reclaim disk space by finding and deleting Python environments you no longer use
25
+ ### Free GBs of disk space by removing unused Python environments
26
26
 
27
- Python environment cleaner for virtualenv, Conda, Poetry, pipx, pyenv, tox, Pipenv, Hatch, uv, caches, and build artifacts.
27
+ Find and delete old `.venv`, conda, poetry, pipenv, uv and more safely, in seconds.
28
+
29
+ ```bash
30
+ uvx killpy --path ~
31
+ ```
28
32
 
29
33
  [Documentation](https://tlaloc-es.github.io/killpy/)
30
34
 
@@ -270,6 +274,7 @@ ______________________________________________________________________
270
274
  killpy stats
271
275
  killpy stats --path ~/projects
272
276
  killpy stats --json
277
+ killpy stats --history # cumulative scan history
273
278
  ```
274
279
 
275
280
  Example output:
@@ -301,6 +306,98 @@ Removes `__pycache__` directories recursively under the target path.
301
306
 
302
307
  ______________________________________________________________________
303
308
 
309
+ ### `killpy doctor` — smart health report
310
+
311
+ ```
312
+ Usage: killpy doctor [OPTIONS]
313
+
314
+ Options:
315
+ --path DIRECTORY Root directory to scan [default: cwd]
316
+ --all Show all environments grouped by category
317
+ (HIGH / MEDIUM / LOW). Default shows only the top 5.
318
+ --json Output as JSON.
319
+ --help Show this message and exit.
320
+ ```
321
+
322
+ `doctor` analyses every detected virtual environment in two phases:
323
+
324
+ **Phase 1 — Scoring (for sorting only)**
325
+
326
+ A numeric score between 0 and 1 is computed from four weighted signals:
327
+
328
+ | Signal | Description |
329
+ |--------|-------------|
330
+ | **Size** | Larger environments score higher (sigmoid-normalised around 500 MB). |
331
+ | **Age** | Days since last access, linear up to 365 days. |
332
+ | **Orphan status** | No `pyproject.toml`, `requirements.txt`, or other project marker found nearby. |
333
+ | **Git inactivity** | The associated git repository has no recent commits. |
334
+
335
+ The score determines *ordering* within each category (highest score listed first). It does **not** determine the category itself.
336
+
337
+ **Phase 2 — Rule-based classification**
338
+
339
+ Category is assigned deterministically by applying the following rules in order:
340
+
341
+ | Priority | Rule | Category |
342
+ |----------|------|----------|
343
+ | 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
344
+ | 2 | No project files **and** `age ≥ 365 days` | `HIGH` |
345
+ | 3 | Active git repository **or** `age < 120 days` | `LOW` |
346
+ | 4 | `age ≥ 120 days` | `MEDIUM` |
347
+ | 5 | Fallback | `LOW` |
348
+
349
+ Age and orphan status are the dominant signals. Size does not affect the category.
350
+
351
+ | Category | Recommended action |
352
+ |----------|--------------------|
353
+ | `HIGH` | Delete — unused and orphaned |
354
+ | `MEDIUM` | Review — possibly unused |
355
+ | `LOW` | Keep — actively used / Keep |
356
+
357
+ Examples:
358
+
359
+ ```bash
360
+ killpy doctor # top 5 offenders in current directory
361
+ killpy doctor --path ~ # scan home folder
362
+ killpy doctor --all # show all environments by category
363
+ killpy doctor --json # machine-readable output
364
+ killpy doctor --path ~/projects --all # full report for a specific tree
365
+ ```
366
+
367
+ Example output (default):
368
+
369
+ ```
370
+ ──────────── Environment Health Report ────────────
371
+ Scanned: /home/user/projects
372
+ Environments found: 18 | Total size: 6.2 GB | Estimated wasted: 3.8 GB
373
+ HIGH (safe to delete): 5 MEDIUM (review): 7 LOW (keep): 6
374
+
375
+ Top 5 Offenders
376
+ ┌──────────────────────────┬────────┬───────────┬───────┬──────────┐
377
+ │ Path │ Size │ Age (days)│ Score │ Category │
378
+ ├──────────────────────────┼────────┼───────────┼───────┼──────────┤
379
+ │ ~/old-project/.venv │ 850 MB │ 312 │ 0.94 │ HIGH │
380
+ │ ~/tutorial2023/.venv │ 420 MB │ 198 │ 0.87 │ HIGH │
381
+ └──────────────────────────┴────────┴───────────┴───────┴──────────┘
382
+
383
+ Recommendation: Run `killpy delete --older-than 180` to free up to 3.8 GB.
384
+ (12 MEDIUM/LOW environment(s) hidden — run with --all to see them)
385
+ ```
386
+
387
+ Use `--all` to see all three category tables at once:
388
+
389
+ ```bash
390
+ killpy doctor --all
391
+ ```
392
+
393
+ The JSON output is useful for scripting or auditing:
394
+
395
+ ```bash
396
+ killpy doctor --json | jq '.suggestions[] | select(.category=="HIGH") | .env_path'
397
+ ```
398
+
399
+ ______________________________________________________________________
400
+
304
401
  ## killpy vs alternatives
305
402
 
306
403
  | Tool | venv | conda | poetry | pipx | pyenv | caches | artifacts | TUI | search | multi-select |
@@ -422,18 +519,44 @@ Each virtual environment is a full copy (or symlinked tree) of a Python interpre
422
519
 
423
520
  ______________________________________________________________________
424
521
 
425
- ## Pre-commit hook
522
+ ## Pre-commit hooks
426
523
 
427
- Keep your repo free of `__pycache__` on every commit:
524
+ `killpy` ships four hooks for [pre-commit](https://pre-commit.com/). Add the ones you need to your `.pre-commit-config.yaml`:
428
525
 
429
526
  ```yaml
430
527
  - repo: https://github.com/Tlaloc-Es/KillPy
431
- rev: 0.16.0
528
+ rev: 0.20.0
529
+ hooks:
530
+ - id: killpy # remove __pycache__ on every commit
531
+ - id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
532
+ - id: killpy-clean-artifacts # remove dist/, build/, *.egg-info before committing
533
+ - id: killpy-remove-venv # remove .venv (manual stage — see below)
534
+ ```
535
+
536
+ | Hook id | What it removes | Default stage |
537
+ |---------|-----------------|---------------|
538
+ | `killpy` | `__pycache__` directories | `pre-commit` |
539
+ | `killpy-clean-caches` | All local cache dirs (`__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`) | `pre-commit` |
540
+ | `killpy-clean-artifacts` | Build artifacts (`dist/`, `build/`, `*.egg-info`) | `pre-commit` |
541
+ | `killpy-remove-venv` | Local `.venv` environments | `manual` |
542
+
543
+ **`killpy-remove-venv`** is staged as `manual` because deleting the environment on every commit would require you to recreate it each time. Run it explicitly when you want a clean slate:
544
+
545
+ ```bash
546
+ pre-commit run killpy-remove-venv --hook-stage manual
547
+ ```
548
+
549
+ Typical minimal setup (safe for daily use):
550
+
551
+ ```yaml
552
+ - repo: https://github.com/Tlaloc-Es/KillPy
553
+ rev: 0.22.0
432
554
  hooks:
433
555
  - id: killpy
434
- pass_filenames: false
435
556
  ```
436
557
 
558
+ Add `killpy-clean-artifacts` if your project generates `dist/` or `build/` locally and you want to guarantee they are never staged by accident.
559
+
437
560
  ______________________________________________________________________
438
561
 
439
562
  ## Safety
@@ -455,6 +578,7 @@ ______________________________________________________________________
455
578
  - [ ] Windows support improvements (pyenv-win, conda on Windows PATH)
456
579
  - [ ] TUI: filter panel, live progress bar, confirmation dialog with total bytes before delete
457
580
  - [ ] Detect unused dependencies inside `pyproject.toml` / `requirements.txt`
581
+ - [ ] Add environment reporting (Python version, installed packages, and versions) to better differentiate between virtual environments with different dependencies
458
582
 
459
583
  ______________________________________________________________________
460
584
 
@@ -2,9 +2,13 @@
2
2
 
3
3
  ![Logo](https://raw.githubusercontent.com/Tlaloc-Es/killpy/master/logo.png)
4
4
 
5
- ### Reclaim disk space by finding and deleting Python environments you no longer use
5
+ ### Free GBs of disk space by removing unused Python environments
6
6
 
7
- Python environment cleaner for virtualenv, Conda, Poetry, pipx, pyenv, tox, Pipenv, Hatch, uv, caches, and build artifacts.
7
+ Find and delete old `.venv`, conda, poetry, pipenv, uv and more safely, in seconds.
8
+
9
+ ```bash
10
+ uvx killpy --path ~
11
+ ```
8
12
 
9
13
  [Documentation](https://tlaloc-es.github.io/killpy/)
10
14
 
@@ -250,6 +254,7 @@ ______________________________________________________________________
250
254
  killpy stats
251
255
  killpy stats --path ~/projects
252
256
  killpy stats --json
257
+ killpy stats --history # cumulative scan history
253
258
  ```
254
259
 
255
260
  Example output:
@@ -281,6 +286,98 @@ Removes `__pycache__` directories recursively under the target path.
281
286
 
282
287
  ______________________________________________________________________
283
288
 
289
+ ### `killpy doctor` — smart health report
290
+
291
+ ```
292
+ Usage: killpy doctor [OPTIONS]
293
+
294
+ Options:
295
+ --path DIRECTORY Root directory to scan [default: cwd]
296
+ --all Show all environments grouped by category
297
+ (HIGH / MEDIUM / LOW). Default shows only the top 5.
298
+ --json Output as JSON.
299
+ --help Show this message and exit.
300
+ ```
301
+
302
+ `doctor` analyses every detected virtual environment in two phases:
303
+
304
+ **Phase 1 — Scoring (for sorting only)**
305
+
306
+ A numeric score between 0 and 1 is computed from four weighted signals:
307
+
308
+ | Signal | Description |
309
+ |--------|-------------|
310
+ | **Size** | Larger environments score higher (sigmoid-normalised around 500 MB). |
311
+ | **Age** | Days since last access, linear up to 365 days. |
312
+ | **Orphan status** | No `pyproject.toml`, `requirements.txt`, or other project marker found nearby. |
313
+ | **Git inactivity** | The associated git repository has no recent commits. |
314
+
315
+ The score determines *ordering* within each category (highest score listed first). It does **not** determine the category itself.
316
+
317
+ **Phase 2 — Rule-based classification**
318
+
319
+ Category is assigned deterministically by applying the following rules in order:
320
+
321
+ | Priority | Rule | Category |
322
+ |----------|------|----------|
323
+ | 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
324
+ | 2 | No project files **and** `age ≥ 365 days` | `HIGH` |
325
+ | 3 | Active git repository **or** `age < 120 days` | `LOW` |
326
+ | 4 | `age ≥ 120 days` | `MEDIUM` |
327
+ | 5 | Fallback | `LOW` |
328
+
329
+ Age and orphan status are the dominant signals. Size does not affect the category.
330
+
331
+ | Category | Recommended action |
332
+ |----------|--------------------|
333
+ | `HIGH` | Delete — unused and orphaned |
334
+ | `MEDIUM` | Review — possibly unused |
335
+ | `LOW` | Keep — actively used / Keep |
336
+
337
+ Examples:
338
+
339
+ ```bash
340
+ killpy doctor # top 5 offenders in current directory
341
+ killpy doctor --path ~ # scan home folder
342
+ killpy doctor --all # show all environments by category
343
+ killpy doctor --json # machine-readable output
344
+ killpy doctor --path ~/projects --all # full report for a specific tree
345
+ ```
346
+
347
+ Example output (default):
348
+
349
+ ```
350
+ ──────────── Environment Health Report ────────────
351
+ Scanned: /home/user/projects
352
+ Environments found: 18 | Total size: 6.2 GB | Estimated wasted: 3.8 GB
353
+ HIGH (safe to delete): 5 MEDIUM (review): 7 LOW (keep): 6
354
+
355
+ Top 5 Offenders
356
+ ┌──────────────────────────┬────────┬───────────┬───────┬──────────┐
357
+ │ Path │ Size │ Age (days)│ Score │ Category │
358
+ ├──────────────────────────┼────────┼───────────┼───────┼──────────┤
359
+ │ ~/old-project/.venv │ 850 MB │ 312 │ 0.94 │ HIGH │
360
+ │ ~/tutorial2023/.venv │ 420 MB │ 198 │ 0.87 │ HIGH │
361
+ └──────────────────────────┴────────┴───────────┴───────┴──────────┘
362
+
363
+ Recommendation: Run `killpy delete --older-than 180` to free up to 3.8 GB.
364
+ (12 MEDIUM/LOW environment(s) hidden — run with --all to see them)
365
+ ```
366
+
367
+ Use `--all` to see all three category tables at once:
368
+
369
+ ```bash
370
+ killpy doctor --all
371
+ ```
372
+
373
+ The JSON output is useful for scripting or auditing:
374
+
375
+ ```bash
376
+ killpy doctor --json | jq '.suggestions[] | select(.category=="HIGH") | .env_path'
377
+ ```
378
+
379
+ ______________________________________________________________________
380
+
284
381
  ## killpy vs alternatives
285
382
 
286
383
  | Tool | venv | conda | poetry | pipx | pyenv | caches | artifacts | TUI | search | multi-select |
@@ -402,18 +499,44 @@ Each virtual environment is a full copy (or symlinked tree) of a Python interpre
402
499
 
403
500
  ______________________________________________________________________
404
501
 
405
- ## Pre-commit hook
502
+ ## Pre-commit hooks
406
503
 
407
- Keep your repo free of `__pycache__` on every commit:
504
+ `killpy` ships four hooks for [pre-commit](https://pre-commit.com/). Add the ones you need to your `.pre-commit-config.yaml`:
408
505
 
409
506
  ```yaml
410
507
  - repo: https://github.com/Tlaloc-Es/KillPy
411
- rev: 0.16.0
508
+ rev: 0.20.0
509
+ hooks:
510
+ - id: killpy # remove __pycache__ on every commit
511
+ - id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
512
+ - id: killpy-clean-artifacts # remove dist/, build/, *.egg-info before committing
513
+ - id: killpy-remove-venv # remove .venv (manual stage — see below)
514
+ ```
515
+
516
+ | Hook id | What it removes | Default stage |
517
+ |---------|-----------------|---------------|
518
+ | `killpy` | `__pycache__` directories | `pre-commit` |
519
+ | `killpy-clean-caches` | All local cache dirs (`__pycache__`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`) | `pre-commit` |
520
+ | `killpy-clean-artifacts` | Build artifacts (`dist/`, `build/`, `*.egg-info`) | `pre-commit` |
521
+ | `killpy-remove-venv` | Local `.venv` environments | `manual` |
522
+
523
+ **`killpy-remove-venv`** is staged as `manual` because deleting the environment on every commit would require you to recreate it each time. Run it explicitly when you want a clean slate:
524
+
525
+ ```bash
526
+ pre-commit run killpy-remove-venv --hook-stage manual
527
+ ```
528
+
529
+ Typical minimal setup (safe for daily use):
530
+
531
+ ```yaml
532
+ - repo: https://github.com/Tlaloc-Es/KillPy
533
+ rev: 0.22.0
412
534
  hooks:
413
535
  - id: killpy
414
- pass_filenames: false
415
536
  ```
416
537
 
538
+ Add `killpy-clean-artifacts` if your project generates `dist/` or `build/` locally and you want to guarantee they are never staged by accident.
539
+
417
540
  ______________________________________________________________________
418
541
 
419
542
  ## Safety
@@ -435,6 +558,7 @@ ______________________________________________________________________
435
558
  - [ ] Windows support improvements (pyenv-win, conda on Windows PATH)
436
559
  - [ ] TUI: filter panel, live progress bar, confirmation dialog with total bytes before delete
437
560
  - [ ] Detect unused dependencies inside `pyproject.toml` / `requirements.txt`
561
+ - [ ] Add environment reporting (Python version, installed packages, and versions) to better differentiate between virtual environments with different dependencies
438
562
 
439
563
  ______________________________________________________________________
440
564
 
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  from pathlib import Path
2
3
 
3
4
  import click
@@ -9,9 +10,11 @@ from killpy.cleaner import Cleaner
9
10
  from killpy.cli import TableApp
10
11
  from killpy.commands.clean import clean
11
12
  from killpy.commands.delete import delete_cmd
13
+ from killpy.commands.doctor import doctor_cmd
12
14
  from killpy.commands.list import list_cmd
13
15
  from killpy.commands.stats import stats_cmd
14
16
  from killpy.files import format_size
17
+ from killpy.intelligence.tracker import UsageTracker
15
18
  from killpy.scanner import Scanner
16
19
 
17
20
 
@@ -63,6 +66,12 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
63
66
  f"freed [bold]{format_size(freed)}[/bold]."
64
67
  )
65
68
 
69
+ # Best-effort: record deletion in history.
70
+ try:
71
+ UsageTracker().record_deletion(freed)
72
+ except Exception: # noqa: BLE001
73
+ pass
74
+
66
75
 
67
76
  @click.group(invoke_without_command=True)
68
77
  @click.option(
@@ -94,6 +103,7 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
94
103
  )
95
104
  @click.pass_context
96
105
  def cli(ctx, path: Path, exclude: str, delete_all: bool, yes: bool):
106
+ logging.basicConfig(level=logging.WARNING)
97
107
  excluded = (
98
108
  {p.strip() for p in exclude.split(",") if p.strip()} if exclude else set()
99
109
  )
@@ -109,6 +119,7 @@ cli.add_command(clean)
109
119
  cli.add_command(list_cmd, name="list")
110
120
  cli.add_command(delete_cmd, name="delete")
111
121
  cli.add_command(stats_cmd, name="stats")
122
+ cli.add_command(doctor_cmd, name="doctor")
112
123
 
113
124
 
114
125
  if __name__ == "__main__":
@@ -8,6 +8,7 @@ from importlib.metadata import PackageNotFoundError, version
8
8
  from pathlib import Path
9
9
  from typing import Any, TypedDict
10
10
 
11
+ from rich.text import Text
11
12
  from textual.app import App, ComposeResult
12
13
  from textual.binding import Binding
13
14
  from textual.widgets import (
@@ -24,9 +25,23 @@ from textual.widgets import (
24
25
  from killpy.cleaner import Cleaner, CleanerError
25
26
  from killpy.cleaners import remove_pycache
26
27
  from killpy.files import format_size
28
+ from killpy.intelligence import SuggestionEngine, score_all
27
29
  from killpy.models import Environment
28
30
  from killpy.scanner import Scanner
29
31
 
32
+ _HEALTH_STYLES: dict[str, tuple[str, str]] = {
33
+ "HIGH": ("HIGH", "bold red"),
34
+ "MEDIUM": ("MED", "bold yellow"),
35
+ "LOW": ("LOW", "bold green"),
36
+ }
37
+
38
+
39
+ def _health_text(category: str) -> Text | str:
40
+ if category in _HEALTH_STYLES:
41
+ label, style = _HEALTH_STYLES[category]
42
+ return Text(label, style=style)
43
+ return ""
44
+
30
45
 
31
46
  def is_venv_tab(func):
32
47
  def wrapper(self, *args, **kwargs):
@@ -68,6 +83,7 @@ class VenvRow(TypedDict):
68
83
  last_modified: str
69
84
  size: int
70
85
  size_human: str
86
+ health: str
71
87
  status: str
72
88
  environment: Environment
73
89
 
@@ -87,6 +103,7 @@ class TableApp(App):
87
103
  "Last Modified",
88
104
  "Size",
89
105
  "Size (Human Readable)",
106
+ "Health",
90
107
  "Status",
91
108
  ]
92
109
  PIPX_HEADERS = ["Package", "Size", "Size (Human Readable)", "Status"]
@@ -95,7 +112,8 @@ class TableApp(App):
95
112
  VENV_COL_LAST_MODIFIED = 2
96
113
  VENV_COL_SIZE = 3
97
114
  VENV_COL_SIZE_HUMAN = 4
98
- VENV_COL_STATUS = 5
115
+ VENV_COL_HEALTH = 5
116
+ VENV_COL_STATUS = 6
99
117
 
100
118
  PIPX_COL_PACKAGE = 0
101
119
  PIPX_COL_SIZE = 1
@@ -125,6 +143,7 @@ class TableApp(App):
125
143
  self._venv_display_indices: list[int] = []
126
144
  self._multi_select_mode: bool = False
127
145
  self._selected_venv_indices: set[int] = set()
146
+ self._health_by_path: dict[str, str] = {}
128
147
  self.cleaner = Cleaner()
129
148
  self.scanner = Scanner(
130
149
  types={
@@ -315,6 +334,7 @@ class TableApp(App):
315
334
  "last_modified": environment.last_accessed_str,
316
335
  "size": environment.size_bytes,
317
336
  "size_human": environment.size_human,
337
+ "health": self._health_by_path.get(str(environment.path), ""),
318
338
  "status": "",
319
339
  "environment": environment,
320
340
  }
@@ -332,6 +352,7 @@ class TableApp(App):
332
352
  row["last_modified"],
333
353
  row["size"],
334
354
  row["size_human"],
355
+ _health_text(row["health"]),
335
356
  row["status"],
336
357
  )
337
358
 
@@ -373,6 +394,7 @@ class TableApp(App):
373
394
  row["last_modified"],
374
395
  row["size"],
375
396
  row["size_human"],
397
+ _health_text(row["health"]),
376
398
  self._compute_row_status(i, row),
377
399
  )
378
400
 
@@ -453,6 +475,11 @@ class TableApp(App):
453
475
  self.venv_rows.sort(
454
476
  key=lambda row: row["size_human"].lower(), reverse=reverse
455
477
  )
478
+ elif column_index == self.VENV_COL_HEALTH:
479
+ _order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "": 3}
480
+ self.venv_rows.sort(
481
+ key=lambda row: _order.get(row["health"], 3), reverse=reverse
482
+ )
456
483
  elif column_index == self.VENV_COL_STATUS:
457
484
  self.venv_rows.sort(key=lambda row: row["status"].lower(), reverse=reverse)
458
485
  else:
@@ -545,6 +572,21 @@ class TableApp(App):
545
572
  status_label.update(
546
573
  f"Found {venv_count} virtual environments and {pipx_count} pipx packages"
547
574
  )
575
+ await self._compute_health_scores()
576
+
577
+ async def _compute_health_scores(self) -> None:
578
+ """Score venv environments and populate the Health column."""
579
+ if not self.venv_rows:
580
+ return
581
+ envs = [row["environment"] for row in self.venv_rows]
582
+ scored = await asyncio.to_thread(lambda: score_all(envs, run_git=False))
583
+ engine = SuggestionEngine()
584
+ suggestions = engine.classify_all(scored)
585
+ for suggestion in suggestions:
586
+ self._health_by_path[str(suggestion.env_path)] = suggestion.category
587
+ for row in self.venv_rows:
588
+ row["health"] = self._health_by_path.get(row["path"], "")
589
+ self.render_venv_table()
548
590
 
549
591
  async def action_clean_pycache(self):
550
592
  total_freed_space = await asyncio.to_thread(remove_pycache, self.root_dir)
@@ -612,11 +654,12 @@ class TableApp(App):
612
654
  return
613
655
  elif current_status == EnvStatus.MARKED_TO_DELETE.value:
614
656
  row["status"] = ""
615
- table.update_cell_at((cursor_cell.row, 5), "")
657
+ table.update_cell_at((cursor_cell.row, self.VENV_COL_STATUS), "")
616
658
  else:
617
659
  row["status"] = EnvStatus.MARKED_TO_DELETE.value
618
660
  table.update_cell_at(
619
- (cursor_cell.row, 5), EnvStatus.MARKED_TO_DELETE.value
661
+ (cursor_cell.row, self.VENV_COL_STATUS),
662
+ EnvStatus.MARKED_TO_DELETE.value,
620
663
  )
621
664
 
622
665
  @is_venv_tab
@@ -633,7 +676,10 @@ class TableApp(App):
633
676
  if self.delete_environment(row["environment"]):
634
677
  self.bytes_release += int(row["size"])
635
678
  row["status"] = EnvStatus.DELETED.value
636
- table.update_cell_at((cursor_cell.row, 5), EnvStatus.DELETED.value)
679
+ table.update_cell_at(
680
+ (cursor_cell.row, self.VENV_COL_STATUS),
681
+ EnvStatus.DELETED.value,
682
+ )
637
683
  self.query_one("#status-label", Label).update(
638
684
  f"{format_size(self.bytes_release)} deleted"
639
685
  )
@@ -773,7 +819,8 @@ class TableApp(App):
773
819
  self._selected_venv_indices.add(data_index)
774
820
  self._update_multi_select_label()
775
821
  table.update_cell_at(
776
- (cursor_cell.row, 5), self._compute_row_status(data_index, row)
822
+ (cursor_cell.row, self.VENV_COL_STATUS),
823
+ self._compute_row_status(data_index, row),
777
824
  )
778
825
 
779
826
  @is_venv_tab
@@ -0,0 +1,65 @@
1
+ """Shared helpers for ``killpy`` commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timedelta, timezone
6
+
7
+ from killpy.models import Environment
8
+
9
+ # Maps user-facing type names (detector names) to the concrete ``Environment.type``
10
+ # values those detectors produce. Two detectors use sub-type tags instead of
11
+ # their own name: VenvDetector (tags: ".venv", "pyvenv.cfg") and CacheDetector
12
+ # (tags: "__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache",
13
+ # "pip-cache", "uv-cache"). Without this mapping, ``--type venv`` and
14
+ # ``--type cache`` would never match anything.
15
+ _TYPE_ALIASES: dict[str, frozenset[str]] = {
16
+ "venv": frozenset({".venv", "pyvenv.cfg"}),
17
+ "cache": frozenset(
18
+ {
19
+ "__pycache__",
20
+ ".mypy_cache",
21
+ ".pytest_cache",
22
+ ".ruff_cache",
23
+ "pip-cache",
24
+ "uv-cache",
25
+ }
26
+ ),
27
+ }
28
+
29
+
30
+ def filter_envs(
31
+ envs: list[Environment],
32
+ types: tuple[str, ...] | None,
33
+ older_than: int | None,
34
+ ) -> list[Environment]:
35
+ """Return a filtered subset of *envs*.
36
+
37
+ Parameters
38
+ ----------
39
+ envs:
40
+ Full list of detected environments.
41
+ types:
42
+ If provided, only environments whose :attr:`~killpy.models.Environment.type`
43
+ matches one of these strings (case-insensitive) are kept. Detector
44
+ names such as ``"venv"`` and ``"cache"`` are automatically expanded to
45
+ their concrete sub-type values via :data:`_TYPE_ALIASES`.
46
+ older_than:
47
+ If provided, only environments not accessed in the last *older_than* days
48
+ are kept.
49
+ """
50
+ now = datetime.now(tz=timezone.utc)
51
+ result = envs
52
+
53
+ if types:
54
+ expanded: set[str] = set()
55
+ for t in types:
56
+ t_lower = t.strip().lower()
57
+ expanded.add(t_lower)
58
+ expanded.update(_TYPE_ALIASES.get(t_lower, frozenset()))
59
+ result = [e for e in result if e.type.lower() in expanded]
60
+
61
+ if older_than is not None:
62
+ cutoff = now - timedelta(days=older_than)
63
+ result = [e for e in result if e.last_accessed < cutoff]
64
+
65
+ return result
@@ -1,4 +1,3 @@
1
- import logging
2
1
  from pathlib import Path
3
2
 
4
3
  import click
@@ -10,8 +9,7 @@ from killpy.files import format_size
10
9
  @click.command()
11
10
  @click.option("--path", default=Path.cwd(), help="Path to the directory to clean")
12
11
  def clean(path):
13
- logging.basicConfig(level=logging.INFO)
14
12
  path = Path(path)
15
- logging.info(f"Executing the clean command in {path}")
13
+ click.echo(f"Cleaning {path}")
16
14
  total_freed_space = remove_pycache(path)
17
- logging.info(f"{format_size(total_freed_space)} deleted")
15
+ click.echo(f"{format_size(total_freed_space)} freed")