cachekat 0.4.8__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.
- cachekat-0.4.8/LICENSE +21 -0
- cachekat-0.4.8/PKG-INFO +109 -0
- cachekat-0.4.8/README.md +79 -0
- cachekat-0.4.8/pyproject.toml +49 -0
- cachekat-0.4.8/setup.cfg +4 -0
- cachekat-0.4.8/src/cachekat/__init__.py +3 -0
- cachekat-0.4.8/src/cachekat/actions.py +127 -0
- cachekat-0.4.8/src/cachekat/cli.py +89 -0
- cachekat-0.4.8/src/cachekat/fsutil.py +25 -0
- cachekat-0.4.8/src/cachekat/i18n.py +265 -0
- cachekat-0.4.8/src/cachekat/keys.py +22 -0
- cachekat-0.4.8/src/cachekat/models.py +39 -0
- cachekat-0.4.8/src/cachekat/probes/__init__.py +5 -0
- cachekat-0.4.8/src/cachekat/probes/docker_df.py +226 -0
- cachekat-0.4.8/src/cachekat/probes/npm_cache.py +72 -0
- cachekat-0.4.8/src/cachekat/probes/pip_cache.py +40 -0
- cachekat-0.4.8/src/cachekat/probes/playwright_cache.py +104 -0
- cachekat-0.4.8/src/cachekat/registry.py +71 -0
- cachekat-0.4.8/src/cachekat/tui.py +322 -0
- cachekat-0.4.8/src/cachekat.egg-info/PKG-INFO +109 -0
- cachekat-0.4.8/src/cachekat.egg-info/SOURCES.txt +32 -0
- cachekat-0.4.8/src/cachekat.egg-info/dependency_links.txt +1 -0
- cachekat-0.4.8/src/cachekat.egg-info/entry_points.txt +2 -0
- cachekat-0.4.8/src/cachekat.egg-info/requires.txt +5 -0
- cachekat-0.4.8/src/cachekat.egg-info/top_level.txt +1 -0
- cachekat-0.4.8/tests/test_actions.py +157 -0
- cachekat-0.4.8/tests/test_cli.py +66 -0
- cachekat-0.4.8/tests/test_docker_probe.py +146 -0
- cachekat-0.4.8/tests/test_i18n.py +51 -0
- cachekat-0.4.8/tests/test_models.py +18 -0
- cachekat-0.4.8/tests/test_npm_probe.py +50 -0
- cachekat-0.4.8/tests/test_pip_probe.py +28 -0
- cachekat-0.4.8/tests/test_playwright_probe.py +55 -0
- cachekat-0.4.8/tests/test_tui.py +136 -0
cachekat-0.4.8/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aeluris
|
|
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.
|
cachekat-0.4.8/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cachekat
|
|
3
|
+
Version: 0.4.8
|
|
4
|
+
Summary: A TUI to see which dev caches eat your disk - and reclaim space safely, risk-graded.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/Aeluris/CacheKat
|
|
7
|
+
Project-URL: Issues, https://github.com/Aeluris/CacheKat/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Aeluris/CacheKat/blob/main/CHANGELOG.md
|
|
9
|
+
Keywords: cache,disk-space,tui,docker,textual,cleanup
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: System :: Filesystems
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: textual>=1.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# CacheKat π
|
|
32
|
+
|
|
33
|
+
[English](README.md) | [δΈζ](README.zh-CN.md)
|
|
34
|
+
|
|
35
|
+
> A TUI to see which dev caches eat your disk β and reclaim space safely,
|
|
36
|
+
> risk-graded. **Never touches your data.**
|
|
37
|
+
|
|
38
|
+
**Main view** β cleanable items on the left (select with space, run with `c`);
|
|
39
|
+
report-only facts on the right (docker volumes, unknown liveness) are visible
|
|
40
|
+
but untouchable β the layout itself teaches the safety model:
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
**Confirm gate** β nothing cleans without an explicit yes; each item carries
|
|
45
|
+
its size and a plain-words consequence, and dry-run (`d`) describes what
|
|
46
|
+
would happen without touching the disk:
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
[](https://github.com/Aeluris/CacheKat/actions/workflows/ci.yml)
|
|
51
|
+
[](https://www.python.org)
|
|
52
|
+
[](LICENSE)
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
|
|
56
|
+
Every developer machine accumulates caches that nobody watches: pip wheels,
|
|
57
|
+
npm tarballs, playwright browsers (old versions pile up per release), docker
|
|
58
|
+
build cache, dangling images, stopped containers... A fresh scan answers the
|
|
59
|
+
only question that matters: **who is eating the disk, and what is actually
|
|
60
|
+
safe to reclaim?**
|
|
61
|
+
|
|
62
|
+
## Safety model (the feature)
|
|
63
|
+
|
|
64
|
+
| Category | Behavior |
|
|
65
|
+
|---|---|
|
|
66
|
+
| True caches (pip, npm, playwright orphans, docker build cache) | Selectable β each row states its plain-words consequence before anything runs |
|
|
67
|
+
| **Docker volumes (your data)** | **Report-only. Never cleaned. No flag unlocks this.** |
|
|
68
|
+
| Docker stopped containers / unused images | Containers: cleanable **one by one**, each warning that `docker rm` deletes the container **itself** (recreate via `docker run`/`compose up`; volumes untouched). Images: report-only until per-image selection ships. |
|
|
69
|
+
| Anything the probe cannot judge | Honest report-only ("liveness unknown"), never a guess |
|
|
70
|
+
|
|
71
|
+
- Every scan is read-only; cleaning is an explicit, confirmed selection.
|
|
72
|
+
- Dry-run mode (`d`): rehearse exactly what would happen β disk untouched.
|
|
73
|
+
- Probes that fail surface as visible error rows, never swallowed.
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
pipx install cachekat
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or from source: `pipx install git+https://github.com/Aeluris/CacheKat.git`
|
|
82
|
+
|
|
83
|
+
## Use
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
cachekat tui # interactive: scan -> select -> confirm -> clean
|
|
87
|
+
cachekat scan # read-only report: who eats the disk
|
|
88
|
+
cachekat scan --json # machine-readable
|
|
89
|
+
# every command accepts --lang auto|en|zh (auto follows the system language)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Docker rows need the docker daemon running β if the TUI shows
|
|
93
|
+
`docker daemon unreachable`, start Docker Desktop and press `r` to rescan.
|
|
94
|
+
|
|
95
|
+
## Roadmap
|
|
96
|
+
|
|
97
|
+
- [x] M0 β skeleton, registry, `scan` CLI, pip probe, CI (win+linux)
|
|
98
|
+
- [x] M1 β docker / npm / playwright probes (orphan detection, volumes red-lined)
|
|
99
|
+
- [x] M2 β textual TUI: select, confirm, clean, dry-run
|
|
100
|
+
- [x] Per-container selection with loud not-a-cache warnings
|
|
101
|
+
- [x] i18n: full en/zh, `--lang auto|en|zh`
|
|
102
|
+
- [ ] per-image selection, more cache families (cargo, gradle, ...)
|
|
103
|
+
|
|
104
|
+
See [CHANGELOG.md](CHANGELOG.md) for details, [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
105
|
+
to help, [AGENTS.md](AGENTS.md) for the engineering rulebook.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
[MIT](LICENSE) β Β© 2026 Aeluris
|
cachekat-0.4.8/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# CacheKat π
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [δΈζ](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
> A TUI to see which dev caches eat your disk β and reclaim space safely,
|
|
6
|
+
> risk-graded. **Never touches your data.**
|
|
7
|
+
|
|
8
|
+
**Main view** β cleanable items on the left (select with space, run with `c`);
|
|
9
|
+
report-only facts on the right (docker volumes, unknown liveness) are visible
|
|
10
|
+
but untouchable β the layout itself teaches the safety model:
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
**Confirm gate** β nothing cleans without an explicit yes; each item carries
|
|
15
|
+
its size and a plain-words consequence, and dry-run (`d`) describes what
|
|
16
|
+
would happen without touching the disk:
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
[](https://github.com/Aeluris/CacheKat/actions/workflows/ci.yml)
|
|
21
|
+
[](https://www.python.org)
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
Every developer machine accumulates caches that nobody watches: pip wheels,
|
|
27
|
+
npm tarballs, playwright browsers (old versions pile up per release), docker
|
|
28
|
+
build cache, dangling images, stopped containers... A fresh scan answers the
|
|
29
|
+
only question that matters: **who is eating the disk, and what is actually
|
|
30
|
+
safe to reclaim?**
|
|
31
|
+
|
|
32
|
+
## Safety model (the feature)
|
|
33
|
+
|
|
34
|
+
| Category | Behavior |
|
|
35
|
+
|---|---|
|
|
36
|
+
| True caches (pip, npm, playwright orphans, docker build cache) | Selectable β each row states its plain-words consequence before anything runs |
|
|
37
|
+
| **Docker volumes (your data)** | **Report-only. Never cleaned. No flag unlocks this.** |
|
|
38
|
+
| Docker stopped containers / unused images | Containers: cleanable **one by one**, each warning that `docker rm` deletes the container **itself** (recreate via `docker run`/`compose up`; volumes untouched). Images: report-only until per-image selection ships. |
|
|
39
|
+
| Anything the probe cannot judge | Honest report-only ("liveness unknown"), never a guess |
|
|
40
|
+
|
|
41
|
+
- Every scan is read-only; cleaning is an explicit, confirmed selection.
|
|
42
|
+
- Dry-run mode (`d`): rehearse exactly what would happen β disk untouched.
|
|
43
|
+
- Probes that fail surface as visible error rows, never swallowed.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
pipx install cachekat
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or from source: `pipx install git+https://github.com/Aeluris/CacheKat.git`
|
|
52
|
+
|
|
53
|
+
## Use
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
cachekat tui # interactive: scan -> select -> confirm -> clean
|
|
57
|
+
cachekat scan # read-only report: who eats the disk
|
|
58
|
+
cachekat scan --json # machine-readable
|
|
59
|
+
# every command accepts --lang auto|en|zh (auto follows the system language)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Docker rows need the docker daemon running β if the TUI shows
|
|
63
|
+
`docker daemon unreachable`, start Docker Desktop and press `r` to rescan.
|
|
64
|
+
|
|
65
|
+
## Roadmap
|
|
66
|
+
|
|
67
|
+
- [x] M0 β skeleton, registry, `scan` CLI, pip probe, CI (win+linux)
|
|
68
|
+
- [x] M1 β docker / npm / playwright probes (orphan detection, volumes red-lined)
|
|
69
|
+
- [x] M2 β textual TUI: select, confirm, clean, dry-run
|
|
70
|
+
- [x] Per-container selection with loud not-a-cache warnings
|
|
71
|
+
- [x] i18n: full en/zh, `--lang auto|en|zh`
|
|
72
|
+
- [ ] per-image selection, more cache families (cargo, gradle, ...)
|
|
73
|
+
|
|
74
|
+
See [CHANGELOG.md](CHANGELOG.md) for details, [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
75
|
+
to help, [AGENTS.md](AGENTS.md) for the engineering rulebook.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
[MIT](LICENSE) β Β© 2026 Aeluris
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cachekat"
|
|
7
|
+
version = "0.4.8"
|
|
8
|
+
description = "A TUI to see which dev caches eat your disk - and reclaim space safely, risk-graded."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
# textual is the single runtime dependency; everything else stdlib
|
|
12
|
+
dependencies = ["textual>=1.0"]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
keywords = ["cache", "disk-space", "tui", "docker", "textual", "cleanup"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: Microsoft :: Windows",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: System :: Filesystems",
|
|
27
|
+
"Topic :: Utilities",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Aeluris/CacheKat"
|
|
32
|
+
Issues = "https://github.com/Aeluris/CacheKat/issues"
|
|
33
|
+
Changelog = "https://github.com/Aeluris/CacheKat/blob/main/CHANGELOG.md"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
cachekat = "cachekat.cli:main"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 100
|
|
46
|
+
src = ["src", "tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "F", "W", "I", "UP", "B"]
|
cachekat-0.4.8/setup.cfg
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Clean actions, one per finding key. The refusal path is the feature.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- REPORT_ONLY findings are refused, always β even in dry-run, even by
|
|
5
|
+
internal callers. There is no flag that unlocks this.
|
|
6
|
+
- dry_run=True must not touch disk or run any CLI, period.
|
|
7
|
+
- Unknown cleanable keys fail loudly ("no clean action implemented") β
|
|
8
|
+
pretending to clean is worse than admitting we can't.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Protocol
|
|
18
|
+
|
|
19
|
+
from cachekat import keys
|
|
20
|
+
from cachekat.i18n import t
|
|
21
|
+
from cachekat.models import Finding, Risk
|
|
22
|
+
|
|
23
|
+
_TIMEOUT = 600 # docker prunes can be slow on big machines
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ActionResult:
|
|
28
|
+
key: str
|
|
29
|
+
ok: bool
|
|
30
|
+
detail: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Runner(Protocol):
|
|
34
|
+
def __call__(self, cmd: list[str]) -> subprocess.CompletedProcess[str]: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _run(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
|
38
|
+
# explicit UTF-8: same zh-Windows reader-thread hazard as probes (a
|
|
39
|
+
# swallowed decode crash surfaces as stdout=None, see docker_df._run)
|
|
40
|
+
return subprocess.run(
|
|
41
|
+
cmd, capture_output=True, text=True,
|
|
42
|
+
encoding="utf-8", errors="replace", timeout=_TIMEOUT,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _cmd_action(
|
|
47
|
+
finding: Finding, dry_run: bool, run: _Runner, cmd: list[str], desc: str
|
|
48
|
+
) -> ActionResult:
|
|
49
|
+
if dry_run:
|
|
50
|
+
return ActionResult(finding.key, True, f"[dry-run] would {desc}")
|
|
51
|
+
cp = run(cmd)
|
|
52
|
+
if cp.returncode == 0:
|
|
53
|
+
tail = (cp.stdout or "").strip().splitlines()
|
|
54
|
+
return ActionResult(finding.key, True, f"{desc}: {tail[-1]}" if tail else desc)
|
|
55
|
+
err = (cp.stderr or cp.stdout or "").strip().splitlines()
|
|
56
|
+
return ActionResult(
|
|
57
|
+
finding.key, False, f"{desc} failed (exit {cp.returncode}): {err[-1] if err else ''}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _rmtree_action(finding: Finding, dry_run: bool) -> ActionResult:
|
|
62
|
+
if finding.path is None:
|
|
63
|
+
return ActionResult(finding.key, False, "no path on finding β cannot remove")
|
|
64
|
+
desc = f"delete {finding.path}"
|
|
65
|
+
if dry_run:
|
|
66
|
+
return ActionResult(finding.key, True, f"[dry-run] would {desc}")
|
|
67
|
+
shutil.rmtree(finding.path)
|
|
68
|
+
return ActionResult(finding.key, True, desc)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# docker prunes: -f skips their interactive prompt; our TUI is the prompt.
|
|
72
|
+
# 2026-10-08 incident: image prune -a / container prune are indiscriminate
|
|
73
|
+
# ("unused"/"stopped" != unwanted β a user's persistent test bench was
|
|
74
|
+
# pruned). Both docker rows are REPORT_ONLY now; only build cache (true
|
|
75
|
+
# cache semantics) remains cleanable here.
|
|
76
|
+
_DOCKER_ACTIONS = {
|
|
77
|
+
keys.DOCKER_BUILD_CACHE: (
|
|
78
|
+
["docker", "builder", "prune", "-f"],
|
|
79
|
+
"docker builder prune -f",
|
|
80
|
+
),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# what each clean actually does, in plain words β the confirm modal shows
|
|
84
|
+
# these BEFORE anything runs (2026-10-08 lesson: item names alone don't warn).
|
|
85
|
+
# Wording lives in cachekat.i18n (en source of truth, zh table).
|
|
86
|
+
_CONSEQUENCE_KEYS = {
|
|
87
|
+
keys.PIP_CACHE: "cons_pip",
|
|
88
|
+
keys.NPM_CACHE: "cons_npm",
|
|
89
|
+
keys.DOCKER_BUILD_CACHE: "cons_build_cache",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def consequence(finding: Finding) -> str:
|
|
94
|
+
"""One plain sentence about what cleaning this finding does."""
|
|
95
|
+
if finding.key.startswith(keys.DOCKER_CONTAINER_PREFIX):
|
|
96
|
+
return t("cons_container")
|
|
97
|
+
if finding.key in _CONSEQUENCE_KEYS:
|
|
98
|
+
return t(_CONSEQUENCE_KEYS[finding.key])
|
|
99
|
+
if finding.key.startswith(keys.PLAYWRIGHT_PREFIX):
|
|
100
|
+
return t("cons_playwright")
|
|
101
|
+
return t("cons_none")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def clean(
|
|
105
|
+
finding: Finding, *, dry_run: bool = False, runner: _Runner | None = None
|
|
106
|
+
) -> ActionResult:
|
|
107
|
+
run = runner if runner is not None else _run
|
|
108
|
+
if finding.risk is not Risk.CLEANABLE:
|
|
109
|
+
return ActionResult(finding.key, False, "refused: report-only is never cleaned")
|
|
110
|
+
if finding.key == keys.PIP_CACHE:
|
|
111
|
+
cmd = [sys.executable, "-m", "pip", "cache", "purge"]
|
|
112
|
+
return _cmd_action(finding, dry_run, run, cmd, "pip cache purge")
|
|
113
|
+
if finding.key == keys.NPM_CACHE:
|
|
114
|
+
return _rmtree_action(finding, dry_run)
|
|
115
|
+
if finding.key.startswith(keys.PLAYWRIGHT_PREFIX):
|
|
116
|
+
return _rmtree_action(finding, dry_run)
|
|
117
|
+
if finding.key.startswith(keys.DOCKER_CONTAINER_PREFIX):
|
|
118
|
+
# name derivation by documented prefix-strip (see cachekat/keys.py):
|
|
119
|
+
# docker names never contain "/", display text is never parsed
|
|
120
|
+
name = finding.key.removeprefix(keys.DOCKER_CONTAINER_PREFIX)
|
|
121
|
+
# no -f: a container that started since the scan refuses naturally,
|
|
122
|
+
# and docker rm never touches volumes
|
|
123
|
+
return _cmd_action(finding, dry_run, run, ["docker", "rm", name], f"docker rm {name}")
|
|
124
|
+
if finding.key in _DOCKER_ACTIONS:
|
|
125
|
+
cmd, desc = _DOCKER_ACTIONS[finding.key]
|
|
126
|
+
return _cmd_action(finding, dry_run, run, cmd, desc)
|
|
127
|
+
return ActionResult(finding.key, False, "no clean action implemented for this finding")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""CLI entry point: `cachekat scan` (read-only report) and `cachekat tui`.
|
|
2
|
+
|
|
3
|
+
Language: every invocation resolves --lang (default auto: system locale,
|
|
4
|
+
zh* -> zh, else en) and resets the i18n state β no cross-call leakage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
|
|
14
|
+
from cachekat import probes # noqa: F401 (probe registration by import)
|
|
15
|
+
from cachekat.i18n import resolve_lang, set_lang, t
|
|
16
|
+
from cachekat.models import Finding, Risk
|
|
17
|
+
from cachekat.registry import run_scan
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def human_size(n: int) -> str:
|
|
21
|
+
steps = ("B", "KiB", "MiB", "GiB", "TiB")
|
|
22
|
+
size = float(n)
|
|
23
|
+
for unit in steps:
|
|
24
|
+
if size < 1024 or unit == steps[-1]:
|
|
25
|
+
return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
|
|
26
|
+
size /= 1024
|
|
27
|
+
return f"{size:.1f} TiB" # pragma: no cover (loop always returns above)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_table(findings: list[Finding]) -> str:
|
|
31
|
+
lines = [
|
|
32
|
+
t("scan_title"),
|
|
33
|
+
"=" * 64,
|
|
34
|
+
]
|
|
35
|
+
for f in findings:
|
|
36
|
+
marker = "~" if f.risk is Risk.CLEANABLE else "!"
|
|
37
|
+
lines.append(
|
|
38
|
+
f"[{marker}] {f.label:<28} {human_size(f.size_bytes):>10}"
|
|
39
|
+
f" {f.detail}"
|
|
40
|
+
)
|
|
41
|
+
lines.append("=" * 64)
|
|
42
|
+
reclaimable = sum(f.size_bytes for f in findings if f.risk is Risk.CLEANABLE)
|
|
43
|
+
lines.append(t("scan_summary", size=human_size(reclaimable)))
|
|
44
|
+
if any(f.risk is Risk.REPORT_ONLY for f in findings):
|
|
45
|
+
lines.append(t("scan_report_note"))
|
|
46
|
+
return "\n".join(lines)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def to_json(findings: list[Finding]) -> str:
|
|
50
|
+
rows = [
|
|
51
|
+
asdict(f)
|
|
52
|
+
| {
|
|
53
|
+
"risk": f.risk.value,
|
|
54
|
+
"path": str(f.path) if f.path is not None else None,
|
|
55
|
+
}
|
|
56
|
+
for f in findings
|
|
57
|
+
]
|
|
58
|
+
return json.dumps(rows, ensure_ascii=False, indent=2)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv: list[str] | None = None) -> int:
|
|
62
|
+
parser = argparse.ArgumentParser(
|
|
63
|
+
prog="cachekat",
|
|
64
|
+
description="See which dev caches eat your disk. Scans are read-only; "
|
|
65
|
+
"cleaning is always an explicit, confirmed choice.",
|
|
66
|
+
)
|
|
67
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
68
|
+
p_scan = sub.add_parser("scan", help="read-only scan and report")
|
|
69
|
+
p_scan.add_argument("--json", action="store_true", help="machine-readable output")
|
|
70
|
+
p_scan.add_argument("--lang", default="auto", choices=["auto", "en", "zh"])
|
|
71
|
+
p_tui = sub.add_parser("tui", help="interactive TUI (select, confirm, clean)")
|
|
72
|
+
p_tui.add_argument("--lang", default="auto", choices=["auto", "en", "zh"])
|
|
73
|
+
args = parser.parse_args(argv)
|
|
74
|
+
|
|
75
|
+
set_lang(resolve_lang(args.lang))
|
|
76
|
+
|
|
77
|
+
if args.command == "tui":
|
|
78
|
+
from cachekat.tui import run_app # lazy: keeps scan light without textual
|
|
79
|
+
|
|
80
|
+
return run_app(lang=args.lang)
|
|
81
|
+
|
|
82
|
+
findings = run_scan()
|
|
83
|
+
if args.command == "scan":
|
|
84
|
+
print(to_json(findings) if args.json else format_table(findings))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
sys.exit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Filesystem helpers shared by probes (single implementation, no copies)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def dir_size_bytes(path: Path) -> int:
|
|
10
|
+
"""Recursive on-disk size. Missing path -> 0.
|
|
11
|
+
|
|
12
|
+
Files that vanish or are locked mid-walk are skipped, so the result is a
|
|
13
|
+
LOWER BOUND. Known limitation, not hidden: for cache directories this
|
|
14
|
+
undershoot is tiny and harmless; revisit if a probe ever needs exactness.
|
|
15
|
+
"""
|
|
16
|
+
if not path.exists():
|
|
17
|
+
return 0
|
|
18
|
+
total = 0
|
|
19
|
+
for root, _dirs, files in os.walk(path):
|
|
20
|
+
for name in files:
|
|
21
|
+
try:
|
|
22
|
+
total += (Path(root) / name).stat().st_size
|
|
23
|
+
except OSError:
|
|
24
|
+
continue # raced-away or locked file (see docstring)
|
|
25
|
+
return total
|