devklean 1.0.0__tar.gz → 1.0.1__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.
- {devklean-1.0.0 → devklean-1.0.1}/CHANGELOG.md +13 -1
- {devklean-1.0.0 → devklean-1.0.1}/PKG-INFO +1 -1
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/_version.py +1 -1
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/main.py +27 -22
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/deletion/metadata.py +8 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/deletion/trash.py +4 -4
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_integrity.py +29 -0
- devklean-1.0.1/tests/test_main_signal.py +34 -0
- {devklean-1.0.0 → devklean-1.0.1}/.gitignore +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/LICENSE +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/README.md +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/pyproject.toml +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/__init__.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/__main__.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/__init__.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/commands/__init__.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/commands/clean.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/commands/common.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/commands/doctor.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/commands/history.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/commands/restore.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/commands/scan.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/confirmation.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/dispatcher.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/cli/parser.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/config/__init__.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/config/defaults.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/config/manager.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/config/models.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/config/paths.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/config/targets.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/deletion/__init__.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/deletion/history.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/deletion/integrity.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/deletion/paths.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/deletion/safety.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/formatting.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/logging_setup.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/models.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/output/__init__.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/output/base.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/output/console.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/output/history_payload.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/output/json.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/output/scan_payload.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/output/sorting.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/output/text.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/output/theme.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/scanner/__init__.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/scanner/filters.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/scanner/scanner.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/src/devklean/tui.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/benchmark_scanner.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/conftest.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_clean_flow.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_cli_parser.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_config.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_config_precedence.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_confirmation.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_console.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_deletion.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_doctor.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_dry_run.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_formatting.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_history.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_integration.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_json_output.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_logging.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_models.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_output_sorting.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_packaging.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_perf_scan.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_restore.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_safety.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_scan_json_cli.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_scan_permissions.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_scan_settings.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_scanner.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_text_renderer.py +0 -0
- {devklean-1.0.0 → devklean-1.0.1}/tests/test_windows_guard.py +0 -0
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.1] - 2026-06-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Pressing Ctrl+C (e.g. at a `clean` confirmation prompt) now exits cleanly with
|
|
15
|
+
a short `Aborted.` notice and exit code 130 (the Unix SIGINT convention)
|
|
16
|
+
instead of dumping a `KeyboardInterrupt` traceback.
|
|
17
|
+
- `doctor` now flags records with an unrecognized `strategy` value as corrupt.
|
|
18
|
+
Stores containing legacy strategy values (e.g. `"recording"`, `"rec"`) were
|
|
19
|
+
previously reported as healthy; the only recognized strategy is `"trash"`.
|
|
20
|
+
|
|
10
21
|
## [1.0.0] - 2026-06-30
|
|
11
22
|
|
|
12
23
|
First stable release. No breaking changes to the CLI since `0.1.0`; this
|
|
@@ -80,6 +91,7 @@ First public pre-release.
|
|
|
80
91
|
per-target sizing.
|
|
81
92
|
- **Packaging** — distributable via `pip`/`pipx`; MIT licensed.
|
|
82
93
|
|
|
83
|
-
[Unreleased]: https://github.com/smurftyy/devklean/compare/v1.0.
|
|
94
|
+
[Unreleased]: https://github.com/smurftyy/devklean/compare/v1.0.1...HEAD
|
|
95
|
+
[1.0.1]: https://github.com/smurftyy/devklean/compare/v1.0.0...v1.0.1
|
|
84
96
|
[1.0.0]: https://github.com/smurftyy/devklean/compare/v0.1.0...v1.0.0
|
|
85
97
|
[0.1.0]: https://github.com/smurftyy/devklean/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devklean
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Clean up common development artifacts (node_modules, .venv, caches) to reclaim disk space
|
|
5
5
|
Project-URL: Homepage, https://github.com/smurftyy/devklean
|
|
6
6
|
Project-URL: Repository, https://github.com/smurftyy/devklean
|
|
@@ -30,32 +30,37 @@ def main() -> None:
|
|
|
30
30
|
if hasattr(sys.stderr, "reconfigure"):
|
|
31
31
|
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
try:
|
|
34
|
+
raw_argv = list(sys.argv)
|
|
35
|
+
argv = resolve_bare_invocation(raw_argv)
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
configure_logging()
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
config_manager = ConfigManager()
|
|
40
|
+
result = config_manager.load_full()
|
|
41
|
+
config = result.config
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
parser = build_parser()
|
|
44
|
+
args = parser.parse_args(argv[1:])
|
|
45
|
+
args._config = config
|
|
46
|
+
config_manager.apply_defaults(args, raw_argv)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
log_invocation(raw_argv, getattr(args, "command", None))
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
# Surface config warnings on stderr so stdout/JSON stay clean.
|
|
51
|
+
if result.warnings:
|
|
52
|
+
warn_console = Console(
|
|
53
|
+
stream=sys.stderr, theme=getattr(config.defaults, "theme", "default")
|
|
54
|
+
)
|
|
55
|
+
for warning in result.warnings:
|
|
56
|
+
warn_console.warning(warning)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
renderer = select_renderer(args, config)
|
|
59
|
+
exit_code = dispatch(args, renderer, config)
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
if exit_code != 0:
|
|
62
|
+
sys.exit(exit_code)
|
|
63
|
+
except KeyboardInterrupt:
|
|
64
|
+
# 130 is the Unix convention for a SIGINT-terminated process (128 + 2).
|
|
65
|
+
print("\nAborted.", file=sys.stderr)
|
|
66
|
+
sys.exit(130)
|
|
@@ -10,6 +10,11 @@ from uuid import uuid4
|
|
|
10
10
|
from devklean.deletion.paths import get_deletion_metadata_dir
|
|
11
11
|
from devklean.models import CleanableItem, DeleteResult
|
|
12
12
|
|
|
13
|
+
# The only recognized strategy value. Shared with trash.py's STRATEGY_NAME so
|
|
14
|
+
# the write side and this validation can't drift; a record with any other value
|
|
15
|
+
# was written by a removed backend and is treated as corrupt.
|
|
16
|
+
TRASH_STRATEGY = "trash"
|
|
17
|
+
|
|
13
18
|
|
|
14
19
|
@dataclass(frozen=True)
|
|
15
20
|
class DeletionMetadataItem:
|
|
@@ -101,6 +106,9 @@ def _parse_record(data: dict[str, object]) -> DeletionMetadataRecord:
|
|
|
101
106
|
):
|
|
102
107
|
raise ValueError("missing or wrong-typed metadata fields")
|
|
103
108
|
|
|
109
|
+
if strategy != TRASH_STRATEGY:
|
|
110
|
+
raise ValueError(f"unrecognized strategy {strategy!r}")
|
|
111
|
+
|
|
104
112
|
return DeletionMetadataRecord(
|
|
105
113
|
schema_version=schema_version,
|
|
106
114
|
deletion_id=deletion_id,
|
|
@@ -4,15 +4,15 @@ from collections.abc import Sequence
|
|
|
4
4
|
|
|
5
5
|
from send2trash import send2trash
|
|
6
6
|
|
|
7
|
-
from devklean.deletion.metadata import MetadataManager
|
|
7
|
+
from devklean.deletion.metadata import TRASH_STRATEGY, MetadataManager
|
|
8
8
|
from devklean.deletion.safety import SafetyValidator
|
|
9
9
|
from devklean.logging_setup import get_logger
|
|
10
10
|
from devklean.models import CleanableItem, DeleteFailure, DeleteResult
|
|
11
11
|
|
|
12
12
|
# The single deletion backend is the native OS trash (Recycle Bin on Windows,
|
|
13
|
-
# ~/.Trash on macOS, the freedesktop trash on Linux) via send2trash.
|
|
14
|
-
#
|
|
15
|
-
STRATEGY_NAME =
|
|
13
|
+
# ~/.Trash on macOS, the freedesktop trash on Linux) via send2trash. The name
|
|
14
|
+
# recorded in metadata/history is the shared constant defined in metadata.py.
|
|
15
|
+
STRATEGY_NAME = TRASH_STRATEGY
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def delete_items(
|
|
@@ -102,6 +102,35 @@ def test_check_integrity_reports_corrupt(tmp_path: Path) -> None:
|
|
|
102
102
|
assert len(report.valid) == 1
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def test_load_records_flags_unrecognized_strategy_as_corrupt(tmp_path: Path) -> None:
|
|
106
|
+
# Records written by older builds carried strategy values ("recording",
|
|
107
|
+
# "rec") that no longer name a real deletion backend. The only valid
|
|
108
|
+
# strategy is "trash"; anything else is semantically corrupt.
|
|
109
|
+
payload = _valid_payload(deletion_id="a", trash_path=None)
|
|
110
|
+
payload["deletion"]["strategy"] = "recording"
|
|
111
|
+
_write(tmp_path, "stale.json", payload)
|
|
112
|
+
|
|
113
|
+
result = MetadataManager(storage_dir=tmp_path).load_records()
|
|
114
|
+
|
|
115
|
+
assert result.records == ()
|
|
116
|
+
assert len(result.corrupt) == 1
|
|
117
|
+
assert "strategy" in result.corrupt[0].reason.lower()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_check_integrity_flags_unrecognized_strategy(tmp_path: Path) -> None:
|
|
121
|
+
meta = tmp_path / "meta"
|
|
122
|
+
_write(meta, "good.json", _valid_payload(deletion_id="a", trash_path=None))
|
|
123
|
+
stale = _valid_payload(deletion_id="b", trash_path=None)
|
|
124
|
+
stale["deletion"]["strategy"] = "rec"
|
|
125
|
+
_write(meta, "stale.json", stale)
|
|
126
|
+
|
|
127
|
+
report = check_integrity(MetadataManager(storage_dir=meta))
|
|
128
|
+
|
|
129
|
+
assert not report.healthy
|
|
130
|
+
assert len(report.valid) == 1
|
|
131
|
+
assert len(report.corrupt) == 1
|
|
132
|
+
|
|
133
|
+
|
|
105
134
|
def test_check_integrity_does_not_treat_missing_trash_as_a_problem(tmp_path: Path) -> None:
|
|
106
135
|
# Items now go to the native OS trash, which devklean does not track. A
|
|
107
136
|
# record that references a no-longer-present path is still a valid record;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Tests for top-level signal handling in the CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
import devklean.cli.main # noqa: F401 (ensure the submodule is imported)
|
|
10
|
+
|
|
11
|
+
# devklean.cli.__init__ rebinds the name "main" to the function, shadowing the
|
|
12
|
+
# submodule attribute, so reach the module object through sys.modules.
|
|
13
|
+
main_module = sys.modules["devklean.cli.main"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_main_handles_keyboard_interrupt_cleanly(tmp_path, monkeypatch, capsys) -> None:
|
|
17
|
+
# Ctrl+C at a confirmation prompt raises KeyboardInterrupt from deep in the
|
|
18
|
+
# call stack. main() must turn that into a clean exit (code 130, the Unix
|
|
19
|
+
# SIGINT convention) with an "Aborted." notice rather than a traceback.
|
|
20
|
+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
|
|
21
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
|
|
22
|
+
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "cache"))
|
|
23
|
+
monkeypatch.setattr("sys.argv", ["devklean", "scan", str(tmp_path)])
|
|
24
|
+
|
|
25
|
+
def _raise(*args, **kwargs):
|
|
26
|
+
raise KeyboardInterrupt
|
|
27
|
+
|
|
28
|
+
monkeypatch.setattr(main_module, "dispatch", _raise)
|
|
29
|
+
|
|
30
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
31
|
+
main_module.main()
|
|
32
|
+
|
|
33
|
+
assert excinfo.value.code == 130
|
|
34
|
+
assert "Aborted." in capsys.readouterr().err
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|