pristine-cli 0.1.0

A language-agnostic reclaimable-space finder and cleaner.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# The tier-one ruleset.
#
# This file is compiled into the binary and can be replaced or extended by a user file (see
# `rules.rs`), so adding an ecosystem is a config edit rather than a release.
#
# EVERY RULE IS MARKER-ANCHORED. There is no such thing as a bare directory name here, and
# adding one is the single change most likely to make pristine delete somebody's source.
# `target` is Rust's and Maven's and SBT's; `vendor` is Go's and Composer's and Bundler's;
# `build` is Gradle's and Dart's, and in a CMake project it is very often hand-written source.
#
# Fields:
#   id           stable identifier. A user rule with the same id REPLACES the built-in one.
#   ecosystem    human name for the ecosystem, and the first half of the label.
#   kind         what the directories are, from a closed vocabulary ORDERED BY WHAT IT COSTS
#                TO LOSE. The second half of the label, and the half a reader can sort, group
#                and filter by:
#                  "unrecoverable" nothing brings it back — secrets, keys, credentials
#                  "dependencies"  installed third-party code — `node_modules`, `.venv`
#                  "build"         compiled output — `target`, `bin`, `obj`, `dist`
#                  "cache"         regenerated automatically — `__pycache__`
#                  "noise"         nothing will miss it — logs, `.DS_Store`
#                No shipped rule uses "unrecoverable" or "noise": those are what the name of a
#                gitignored FILE can say, and a marker proves a project's type rather than
#                that a directory is the only copy of something. A user rule naming a
#                `secrets/` directory may use it. A kind NAMES what a thing is and changes
#                what no verb does, so that directory is marked and removed like any other —
#                what keeps something precious off a screen is the lens, not its label.
#                A RULE WHOSE TARGETS DO NOT ALL SHARE ONE KIND IS TWO RULES. That is why
#                `python` and `python-caches` are separate, and why Gradle's `build` and
#                `.gradle` are: one rule covering both could label only one of them honestly.
#   markers      evidence that the anchor directory really is a project of this kind. Literal
#                names are checked with one stat; a name containing `*` or `?` costs one
#                read_dir, so prefer literals.
#   markers_required  "any" (default) or "all".
#   targets      paths relative to the anchor. Multi-segment paths (`vendor/bundle`) and
#                globbed final segments (`bazel-*`) are both allowed.
#   anchor       where the markers are looked for, relative to the matched directory:
#                  "parent"   (default) the directory the target path hangs off — for a
#                             single-segment target that is the matched directory's parent.
#                  "self"     the matched directory itself. This is how an ambiguous name is
#                             made safe: a CMake `build/` is reclaimable only when it
#                             contains the `CMakeCache.txt` that CMake generated there.
#                  "ancestor" the nearest ancestor carrying a marker, searched upward to the
#                             scan root. For artefacts that are scattered through a project
#                             rather than parked at its root, like `__pycache__`.
#   note         optional caveat surfaced next to the hit.
#
# THERE IS NO FIELD FOR THE COMMAND THAT REBUILDS A DIRECTORY, and there deliberately is not.
# It cannot be one: `regenerate = "npm install"` is a guess about a package manager nothing
# here checked, and half the ruleset could only fill it in with a sentence ("rebuilt on the
# next import"), which is not a command. The kind carries what that field was actually for —
# a cache is free, an output is a compile, dependencies are a network fetch — and unlike a
# command string it is true without knowing anything about the machine.
#
# Rules are evaluated in order and the first match wins, so put the more specific rule first
# where two could claim the same directory.
#
# ---------------------------------------------------------------------------------------
# `exclude`: where NOT to look. A user file only — this one ships none, and will not.
# ---------------------------------------------------------------------------------------
#
# A rule says what a directory IS, which is knowledge about an ecosystem and true everywhere.
# Where not to look is knowledge about one disk, so it can only come from the person holding
# it. A shipped list of "system directories" would be a guess about an operating system that
# moves underneath it, and a cleaner that quietly skipped somewhere would be lying about its
# own totals.
#
# Gitignore syntax, matched against paths under the scan root, so a pattern containing a `/`
# is anchored to that root and one without matches at any depth. `!` takes something back,
# which is the shape a real exclusion usually has — a directory full of things the system
# refuses to open, with something worth reclaiming sitting among them:
#
#   exclude = [
#     "Library/Application Support/*",
#     "!Library/Application Support/Zed",
#   ]
#
# Excluded paths are COUNTED AND REPORTED (`excluded: 114 paths not walked`). That is the
# point of them being a list rather than a silence: the totals are still not the whole tree,
# and a run that hid that would be lying in the other direction. `--exclude <GLOB>` adds to
# this list for one run.

[[rules]]
id = "node"
ecosystem = "Node"
kind = "dependencies"
markers = ["package.json"]
targets = ["node_modules"]

[[rules]]
id = "cargo"
ecosystem = "Rust"
kind = "build"
markers = ["Cargo.toml"]
targets = ["target"]

[[rules]]
id = "go"
ecosystem = "Go"
kind = "dependencies"
markers = ["go.mod"]
targets = ["vendor"]

[[rules]]
id = "python"
ecosystem = "Python"
kind = "dependencies"
markers = [
    "pyproject.toml",
    "requirements.txt",
    "setup.py",
    "setup.cfg",
    "Pipfile",
]
targets = [".venv", "venv", ".tox"]

# Split from `python` because these are scattered through a package tree rather than parked
# at its root: `src/pkg/sub/__pycache__` is normal and its parent holds no manifest. The kind
# differs too, and that is the other half of the reason: an interpreter cache is not a virtual
# environment full of downloaded wheels.
[[rules]]
id = "python-caches"
ecosystem = "Python"
kind = "cache"
anchor = "ancestor"
markers = [
    "pyproject.toml",
    "requirements.txt",
    "setup.py",
    "setup.cfg",
    "Pipfile",
]
targets = ["__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"]

[[rules]]
id = "pixi"
ecosystem = "Pixi"
kind = "dependencies"
markers = ["pixi.toml", "pixi.lock"]
targets = [".pixi"]

[[rules]]
id = "jupyter"
ecosystem = "Jupyter"
kind = "cache"
markers = ["*.ipynb"]
targets = [".ipynb_checkpoints"]

[[rules]]
id = "dotnet"
ecosystem = ".NET"
kind = "build"
markers = ["*.csproj", "*.fsproj", "*.vbproj", "*.sln", "*.slnx"]
targets = ["bin", "obj"]

[[rules]]
id = "gradle"
ecosystem = "Gradle"
kind = "build"
markers = [
    "build.gradle",
    "build.gradle.kts",
    "settings.gradle",
    "settings.gradle.kts",
]
targets = ["build"]

[[rules]]
id = "gradle-caches"
ecosystem = "Gradle"
kind = "cache"
markers = [
    "build.gradle",
    "build.gradle.kts",
    "settings.gradle",
    "settings.gradle.kts",
]
targets = [".gradle"]

[[rules]]
id = "maven"
ecosystem = "Maven"
kind = "build"
markers = ["pom.xml"]
targets = ["target"]

[[rules]]
id = "sbt"
ecosystem = "SBT"
kind = "build"
markers = ["build.sbt"]
targets = ["target", "project/target"]

[[rules]]
id = "composer"
ecosystem = "PHP / Composer"
kind = "dependencies"
markers = ["composer.json"]
targets = ["vendor"]

# `.bundle` is neither installed code nor output, and the judgement is recorded rather than
# left to be re-litigated: it is the state `bundle install --path` writes beside the gems it
# vendored, so it shares their kind and comes back with them.
[[rules]]
id = "bundler"
ecosystem = "Ruby / Bundler"
kind = "dependencies"
markers = ["Gemfile"]
targets = ["vendor/bundle", ".bundle"]

[[rules]]
id = "elixir"
ecosystem = "Elixir"
kind = "dependencies"
markers = ["mix.exs"]
targets = ["deps"]

[[rules]]
id = "elixir-build"
ecosystem = "Elixir"
kind = "build"
markers = ["mix.exs"]
targets = ["_build"]

[[rules]]
id = "swift"
ecosystem = "Swift"
kind = "build"
markers = ["Package.swift"]
targets = [".build"]

[[rules]]
id = "cocoapods"
ecosystem = "CocoaPods"
kind = "dependencies"
markers = ["Podfile"]
targets = ["Pods"]

[[rules]]
id = "dart"
ecosystem = "Dart / Flutter"
kind = "build"
markers = ["pubspec.yaml"]
targets = ["build"]

[[rules]]
id = "dart-caches"
ecosystem = "Dart / Flutter"
kind = "cache"
markers = ["pubspec.yaml"]
targets = [".dart_tool"]

[[rules]]
id = "zig"
ecosystem = "Zig"
kind = "build"
markers = ["build.zig"]
targets = ["zig-out"]

[[rules]]
id = "zig-caches"
ecosystem = "Zig"
kind = "cache"
markers = ["build.zig"]
targets = [".zig-cache", "zig-cache"]

[[rules]]
id = "stack"
ecosystem = "Haskell / Stack"
kind = "build"
markers = ["stack.yaml"]
targets = [".stack-work"]

[[rules]]
id = "cabal"
ecosystem = "Haskell / Cabal"
kind = "build"
markers = ["*.cabal", "cabal.project"]
targets = ["dist-newstyle", "dist"]

# Self-anchored, and this is the rule the whole marker-anchoring argument was made for: a
# directory named `build` in a CMake project is as likely to be hand-written source as it is
# to be output. CMake writes `CMakeCache.txt` into the binary directory it owns, and nothing
# else does, so requiring the marker INSIDE the candidate is what makes the name safe.
[[rules]]
id = "cmake"
ecosystem = "CMake"
kind = "build"
anchor = "self"
markers = ["CMakeCache.txt"]
targets = ["build", "_build", "out", "cmake-build-*", "build-*"]

# `Library` is the asset import cache and `Temp` is play-mode and build scratch. A cache by
# kind, but an expensive one to refill, which is what the note is for: the kind says how the
# directory comes back and the note says what it costs when that is unusual.
[[rules]]
id = "unity"
ecosystem = "Unity"
kind = "cache"
markers = ["ProjectSettings", "Assets"]
markers_required = "all"
targets = ["Library", "Temp"]
note = "a Unity reimport is slow — minutes to hours on a large project"

# `Obj` holds compiled intermediates from script compilation, which is output rather than an
# import cache — so it is its own rule, on the invariant one row up.
[[rules]]
id = "unity-build"
ecosystem = "Unity"
kind = "build"
markers = ["ProjectSettings", "Assets"]
markers_required = "all"
targets = ["Obj"]

# `Build/` is NOT claimed, though kondo claims it. An Unreal project keeps authored
# per-platform build configuration under it — `Build/IOS/` provisioning and icons,
# `Build/Android/` manifests and resources — so a `.uproject` beside it proves the project's
# type and not that the directory is generated. That is the same mistake as matching a bare
# `build`, one rule further in. `Saved/` is out for the same reason: logs, configs, autosaves.
[[rules]]
id = "unreal"
ecosystem = "Unreal Engine"
kind = "build"
markers = ["*.uproject"]
targets = ["Binaries", "Intermediate"]
note = "`Build/` and `Saved/` are not claimed: both hold authored files alongside output"

[[rules]]
id = "unreal-caches"
ecosystem = "Unreal Engine"
kind = "cache"
markers = ["*.uproject"]
targets = ["DerivedDataCache"]

[[rules]]
id = "godot"
ecosystem = "Godot"
kind = "cache"
markers = ["project.godot"]
targets = [".godot", ".import"]

[[rules]]
id = "react-native"
ecosystem = "React Native"
kind = "build"
markers = ["metro.config.js", "metro.config.cjs", "react-native.config.js"]
targets = ["android/build", "android/app/build", "ios/build"]

[[rules]]
id = "react-native-caches"
ecosystem = "React Native"
kind = "cache"
markers = ["metro.config.js", "metro.config.cjs", "react-native.config.js"]
targets = [".expo"]

[[rules]]
id = "nextjs"
ecosystem = "Next.js"
kind = "build"
markers = [
    "next.config.js",
    "next.config.mjs",
    "next.config.cjs",
    "next.config.ts",
]
targets = [".next"]

[[rules]]
id = "turborepo"
ecosystem = "Turborepo"
kind = "cache"
anchor = "ancestor"
markers = ["turbo.json"]
targets = [".turbo"]

[[rules]]
id = "nx"
ecosystem = "Nx"
kind = "build"
markers = ["nx.json"]
targets = ["dist"]

[[rules]]
id = "nx-caches"
ecosystem = "Nx"
kind = "cache"
markers = ["nx.json"]
targets = [".nx/cache", ".nx/workspace-data"]

[[rules]]
id = "terraform"
ecosystem = "Terraform"
kind = "dependencies"
markers = ["*.tf", "*.tf.json"]
targets = [".terraform"]

[[rules]]
id = "terraform-caches"
ecosystem = "Terraform"
kind = "cache"
markers = ["*.tf", "*.tf.json"]
targets = [".terragrunt-cache"]

# Bazel's `bazel-*` entries are symlinks into an output base that usually lives outside the
# scan root, so the bytes they report are the links' own and not the output base's. They are
# still worth surfacing: removing them is what unpins a stale output base.
[[rules]]
id = "bazel"
ecosystem = "Bazel"
kind = "build"
markers = ["MODULE.bazel", "WORKSPACE", "WORKSPACE.bazel"]
targets = ["bazel-*"]
note = "these are symlinks; the space is in the output base, which `bazel clean --expunge` frees"