dev_prune/constants.rs
1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Centralized application constants and default configurations.
5//
6// Serves as the single source of truth for app metadata, versioning,
7// default thresholds, and file paths.
8
9/// Application crate version derived dynamically from `Cargo.toml`.
10pub const VERSION: &str = env!("CARGO_PKG_VERSION");
11
12/// The marker `devp trust` looks for when reading another copy's version.
13///
14/// A binary cannot be asked its version without running it, and `devp trust` must not
15/// run the files it reports on — on Windows one of them is `devpw.exe`, which is linked
16/// for the GUI subsystem, so a shell that invokes it waits forever. So every build
17/// carries its version as a fixed string in its own bytes and the report reads it off
18/// the disk.
19pub const VERSION_STAMP_MARK: &str = "dev-prune-build-stamp/";
20
21/// The stamp this build carries, which is what the scan above finds.
22///
23/// The mark is written out again here rather than composed from `VERSION_STAMP_MARK`,
24/// because `concat!` takes literals and not constants. That leaves two copies of the
25/// mark in every binary — this one, and the search literal itself — so the scan
26/// validates what follows each hit and keeps the first that parses as a version rather
27/// than trusting the first hit.
28pub const VERSION_STAMP: &str =
29 concat!("dev-prune-build-stamp/", env!("CARGO_PKG_VERSION"), "/end");
30
31/// The stamp, kept in the compiled binary by being an exported symbol nothing may drop.
32///
33/// `#[used]` binds the compiler and not the linker, and both `/OPT:REF` and
34/// `--gc-sections` are free to remove a static that nothing reads. Exporting the symbol
35/// as well is what survives them. Neither is proof on three platforms, which is why
36/// `trust`'s tests scan the test executable for this string rather than assume it is
37/// there — a stamp the linker quietly removed would turn every version in the report
38/// into "not stamped", and nothing else would notice.
39#[used]
40#[unsafe(no_mangle)]
41pub static DEV_PRUNE_BUILD_STAMP: &str = VERSION_STAMP;
42
43/// Minimum supported Rust version, derived dynamically from `rust-version` in
44/// `Cargo.toml` — which is the single source of truth for the MSRV. Only `cargo
45/// install` users ever meet it; every other channel ships a prebuilt binary.
46pub const MSRV: &str = env!("CARGO_PKG_RUST_VERSION");
47
48/// Application name.
49pub const APP_NAME: &str = "dev-prune";
50
51/// Author of dev-prune.
52pub const AUTHOR: &str = "VKrishna04";
53
54/// Canonical source repository.
55///
56/// The one in `Cargo.toml` is only visible to people who already found the crate. This
57/// one is compiled into the binary, so a copy of the executable still says where it came
58/// from.
59pub const REPO_URL: &str = "https://github.com/Life-Experimentalist/dev-prune";
60
61/// Project homepage.
62pub const HOMEPAGE_URL: &str = "https://devprune.vkrishna04.me";
63
64/// The one-line credit printed under interactive output.
65///
66/// Deliberately plain text in plain sight: it is not obfuscated, not assembled at
67/// runtime, and not checked anywhere. Anyone may fork this project and change this line
68/// — the Apache-2.0 licence says so, and nothing in the code argues. It exists so that
69/// the common case, someone running the published binary, shows where it came from.
70pub const ATTRIBUTION_LINE: &str =
71 "dev-prune · made with ♥ by VKrishna04 · github.com/Life-Experimentalist/dev-prune";
72
73/// What the banner says the tool is, in one line.
74///
75/// The framing people arrive with is "the `node_modules` cleaner". That was fair when
76/// there were four adapters and has been wrong for a long time — and the banner is the
77/// first screen of every interactive command, so it is the cheapest place to correct it.
78/// No count of package managers here on purpose: a number in a banner is a number that
79/// goes stale the next time an adapter lands.
80pub const TAGLINE: &str = "Every dependency directory on this machine, in one command.";
81
82/// The half of the pitch that is a promise rather than a description.
83///
84/// It sits under [`TAGLINE`] because "deletes directories" is the part a new user is
85/// right to be nervous about, and the answer to it should not be three screens away in
86/// the README. Both halves of the sentence are load-bearing: the lockfile rule is what
87/// the engine actually enforces, and `restore --last-run` is what covers the case where
88/// the rule was satisfied and the user still wanted the directory back. It named `undo`
89/// for six releases, which reverses an `init` or a `link` and has never put a directory
90/// back — the one promise on the first screen pointed at the wrong command.
91pub const TAGLINE_SAFETY: &str =
92 "Only what a lockfile can rebuild, and `devp restore --last-run` puts it back.";
93
94/// The licence sentence under the declaration, in both wizard paths.
95///
96/// Apache-2.0 needs no click-through and this is not one: the licence governs use
97/// whether or not anybody reads this line. It is here because the screen it sits on is
98/// the first thing a new user meets, and a tool that deletes directories should say what
99/// it does and does not promise at that moment rather than in a file called LICENSE.md.
100/// Sections 7 and 8 are named so the disclaimer can be checked rather than taken on
101/// trust.
102pub const LICENCE_NOTICE: &str =
103 "Apache-2.0 sections 7-8: no warranty, no liability. Using it accepts that.";
104
105/// The body of `devp --version`.
106///
107/// Built at runtime rather than with `concat!`, which only takes literals and would mean
108/// spelling the author and the URL a second time. Two copies of a string are two things
109/// that can disagree, and this one exists precisely so that a stray copy of the binary
110/// can still be traced back.
111pub static LONG_VERSION: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
112 format!(
113 "{VERSION}\n\
114 author: {AUTHOR}\n\
115 repository: {REPO_URL}\n\
116 homepage: {HOMEPAGE_URL}\n\
117 license: Apache-2.0"
118 )
119});
120
121/// How many prune passes `devp stats` keeps a summary of.
122///
123/// The registry is rewritten in full on every save, so this list is a file-size decision
124/// as much as a display one. Fifty passes is roughly a year of a fortnightly schedule.
125pub const PRUNE_HISTORY_LIMIT: usize = 50;
126
127/// Name of the append-only prune log, beside the registry.
128///
129/// Separate from `registry.json` because the two have opposite shapes. The registry is
130/// rewritten in full on every save and every command loads it, so a per-directory record
131/// of every pass ever run would be paid for by `devp status`. This file is read by one
132/// command.
133pub const PRUNE_LOG_FILENAME: &str = "prune-log.jsonl";
134
135/// How many passes the prune log keeps in full, per-directory detail.
136///
137/// Not the same decision as [`PRUNE_HISTORY_LIMIT`], and deliberately a different number:
138/// that one is four integers per pass inside a file every command parses, this one is a
139/// line per pass in a file only `devp history` opens. A hundred passes of a realistic
140/// size is well under a megabyte; the oldest come off the front.
141pub const PRUNE_LOG_LIMIT: usize = 100;
142
143/// The release `devp history` starts recording per-directory detail in.
144///
145/// Passes older than this exist — [`PRUNE_HISTORY_LIMIT`] summaries of them are in the
146/// registry — but with four numbers each and no directory list. `devp history` shows them
147/// anyway, marked, because a machine that has pruned for a year and shows an empty log
148/// reads as data loss rather than as a format that changed.
149pub const PRUNE_LOG_STARTS_AT: &str = "1.17.0";
150
151/// How many restore measurements one adapter's throughput average is worth.
152///
153/// Past this, the running totals are halved before the new sample is added, so the
154/// average follows the machine rather than remembering a disk it no longer has. A plain
155/// lifetime mean would quote a spinning-rust number years after the SSD went in.
156pub const RESTORE_RATE_SAMPLE_CAP: u32 = 20;
157
158/// The shortest restore worth learning a throughput from, in milliseconds.
159///
160/// A restore that returned in under this is a manager deciding it had nothing to do —
161/// the packages were still in its cache, or already on disk. Averaging those in would
162/// claim a rate no cold restore can reach.
163pub const RESTORE_RATE_MIN_MILLIS: u64 = 250;
164
165/// The release that started recording per-repository totals and the pass history.
166///
167/// A machine that pruned for months on 1.0.0 has a large lifetime total and no history at
168/// all, and reading that as "nothing was ever pruned here" would be wrong. Both `devp
169/// stats` and its `--json` document quote this version so the gap is explained rather than
170/// looking like data loss. It is deliberately not [`VERSION`]: it names the release the
171/// format changed in, and does not move again.
172pub const HISTORY_STARTS_AT: &str = "1.1.0";
173
174/// Default idle threshold in days before a repository is eligible for pruning.
175pub const DEFAULT_IDLE_DAYS: u64 = 15;
176
177/// Default idle threshold for *build-tree* directories, in days.
178///
179/// Applies to every adapter that answers [`crate::adapters::PackageManager::opt_in`].
180/// Deliberately longer than [`DEFAULT_IDLE_DAYS`],
181/// because those directories come back by recompiling the whole project rather than by
182/// re-downloading a dependency tree, so the bar for "nobody will miss this" sits
183/// higher. The engine applies `max(build_idle_days, idle_days)`.
184///
185/// Three times the dependency window and no more: 60 days was long enough that a
186/// project touched once a quarter never became a candidate at all, which is not
187/// caution, it is the feature never firing.
188pub const DEFAULT_BUILD_IDLE_DAYS: u64 = 45;
189
190/// The cache size cap the first run suggests, in gibibytes.
191///
192/// Not a default: `cache_max_gb` is empty until someone puts a number in it, and an
193/// empty map means no cache is ever called too big. This is the figure the suggestions
194/// screen and `devp config recommended` offer, and 10 GiB is where it sits because that
195/// is the size at which a download cache stops being a time-saver and starts being the
196/// largest directory on the disk — a `uv` cache measured on the author's machine had
197/// passed it while every project it served fit in a tenth of that.
198pub const RECOMMENDED_CACHE_MAX_GB: u64 = 10;
199
200/// The `cache_max_gb` key that caps every manager without naming one.
201///
202/// A word rather than `*`, because the caps are typed at a shell: a glob would need
203/// quoting in every example the docs print and in every one somebody copies, and would
204/// be silently expanded by the shells that do not need it. `default=10,npm=4` also says
205/// what it does without a legend.
206pub const CACHE_CAP_DEFAULT_KEY: &str = "default";
207
208/// What the first run suggests for `cache_max_gb`.
209///
210/// [`RECOMMENDED_CACHE_MAX_GB`] written out, because the suggestion table holds
211/// `&'static str` values and there is no formatting at compile time. A test in
212/// `commands::config` fails if the two ever stop agreeing.
213pub const RECOMMENDED_CACHE_CAP: &str = "default=10";
214
215/// One gibibyte, for turning `cache_max_gb` into the byte count a cache is measured in.
216pub const BYTES_PER_GIB: u64 = 1024 * 1024 * 1024;
217
218/// Shown while `devp trust` and the configurator ask the OS about the scheduler and
219/// Git hooks, then overwritten in place. Kept here because its width is what the
220/// erase writes over, so the two must not be able to drift apart.
221pub const READING_MACHINE: &str = "Reading this machine (scheduler, Git hooks)...";
222
223/// Default interval in days between background daemon prune runs.
224pub const DEFAULT_CHECK_INTERVAL_DAYS: u64 = 2;
225
226/// Whether the setup pass installs the OS scheduler.
227///
228/// On. A pruner that has to be remembered is a pruner that never runs; the scheduled
229/// pass is the product, not an extra. It is still bounded by everything an interactive
230/// run is bounded by — idle threshold, lockfile verification, per-repo opt-outs — and
231/// `devp daemon uninstall` (or `devp config set auto_daemon false`) removes it.
232pub const DEFAULT_AUTO_DAEMON: bool = true;
233
234/// Whether the setup pass installs the global Git auto-registration hooks.
235///
236/// On, but conditionally: installation is skipped, not forced, when `core.hooksPath`
237/// already belongs to husky, pre-commit or lefthook.
238pub const DEFAULT_AUTO_HOOKS: bool = true;
239
240/// Whether dev-prune installs its missing integrations by itself.
241///
242/// On. The pass runs once per installed version — on first run, and again after an
243/// upgrade — and only creates what is absent. Set to `false`, or export
244/// `DEV_PRUNE_NO_AUTO_SETUP`, to manage the integrations entirely by hand.
245///
246/// The environment variable is symmetric: with it set, `devp uninstall` leaves those
247/// same hand-managed integrations — the scheduler, the agent skills, the install
248/// directories guessed from the home folder — alone too, and says so. "Entirely by
249/// hand" has to include the removal, or the variable's promise only holds until the
250/// day you uninstall.
251pub const DEFAULT_AUTO_SETUP: bool = true;
252
253/// Default for whether `link` and `init` write a `.devprune.json` into repositories
254/// they register.
255///
256/// Off: most repositories are fine on the defaults, and a config file dropped into
257/// every repo is litter. The setting exists for people who tune repositories
258/// individually often enough that creating the file by hand every time is the chore.
259pub const DEFAULT_AUTO_CONFIG: bool = false;
260
261/// Default setting for requiring interactive confirmation before pruning.
262pub const DEFAULT_REQUIRE_CONFIRMATION: bool = true;
263
264/// Language for dev-prune's own headings and summary lines.
265///
266/// English, and English is also the fallback for every key a translation has not
267/// reached yet -- see [`crate::i18n`]. Deliberately not derived from the operating
268/// system locale: a machine configured in one language has said nothing about what
269/// language its owner wants their build tools in.
270pub const DEFAULT_LANGUAGE: &str = "en";
271
272/// Default size floor, in MiB, below which a bloat directory is left alone.
273///
274/// Zero — every recognised directory is a candidate. Raising it trades a little disk
275/// space for fewer reinstalls: deleting a 3 MiB `node_modules` costs a full `npm ci`
276/// and reclaims almost nothing.
277pub const DEFAULT_MIN_SIZE_MB: u64 = 0;
278
279/// How far below a repository root project discovery descends, by default.
280///
281/// Six covers `packages/scope/name/…` monorepo layouts with room to spare while keeping
282/// the walk bounded on repositories with deep source trees. Configurable with
283/// `devp config set scan_depth`, and per repository with `"scan_depth"` in
284/// `.devprune.json`, because "deep enough" is a property of the layout, not of the tool.
285pub const DEFAULT_SCAN_DEPTH: usize = 6;
286
287/// Upper bound accepted for `scan_depth`.
288///
289/// Not a matter of taste. The walk is breadth-first over every directory that is not
290/// excluded, so cost grows with the tree, and a repository with a deep generated tree
291/// (a Bazel `bazel-out`, a `.terraform` provider cache) can turn an unbounded walk into
292/// a multi-minute stall on a background pass nobody is watching.
293pub const MAX_SCAN_DEPTH_LIMIT: usize = 32;
294
295/// Ceiling on the threads `devp status` uses to size repositories.
296///
297/// The scan is one independent file-system walk per repository, so it is bound by the
298/// disk rather than the CPU and oversubscribing the cores is what makes it fast. The
299/// ceiling exists anyway: past this point a spinning disk spends its time seeking
300/// between trees instead of reading them, and a laptop under a background pass should
301/// still be usable.
302pub const STATUS_SCAN_MAX_THREADS: usize = 32;
303
304/// Threads per reported core for the status scan. Above one because the scan waits on
305/// the disk far more than on the CPU; well below what the disk will queue, because past
306/// that point the extra threads only take turns.
307pub const STATUS_SCAN_THREADS_PER_CORE: usize = 2;
308
309/// Overrides the computed status-scan thread count. Clamped to
310/// [`STATUS_SCAN_MAX_THREADS`]; `1` forces a sequential scan.
311pub const STATUS_SCAN_THREADS_ENV: &str = "DEV_PRUNE_SCAN_THREADS";
312
313/// How many lines of a failed command's output are relayed into a report.
314///
315/// A failing `npm ci` prints its whole usage screen. Six lines is enough for the error
316/// and its cause, and short enough that the prune report around it is still readable;
317/// the rest is reachable through the log file the condensed output still names.
318pub const TOOL_OUTPUT_MAX_LINES: usize = 6;
319
320/// Directory names that mean "the contents of this are disposable".
321///
322/// Matched against a repository's *ancestors*, never against the repository directory
323/// itself: a project may legitimately be called `cache`, but a checkout sitting inside
324/// one is something a tool put there. Editor and agent plugin managers clone into
325/// directories like `~/.claude/plugins/cache/temp_git_<id>`, which is nowhere near the OS
326/// temp directory and is deleted just as fast; twenty-eight of those reached one
327/// registry before this list existed.
328pub const EPHEMERAL_ANCESTORS: &[&str] = &["cache", ".cache", "Cache", "Caches", "tmp", ".tmp"];
329
330/// Repository directory *names* that mean the same thing, wherever they are.
331///
332/// The ancestor list above only catches a throwaway clone that a tool was polite enough
333/// to put under a directory called `cache`. These prefixes catch the clone itself:
334/// `temp_git_1787245534782` is a checkout some plugin manager made, named after the
335/// millisecond it made it, and it will be gone before the next prune pass. Registering
336/// one strands a dead entry in the registry the moment the tool cleans up after itself.
337///
338/// A prefix rather than a substring, and deliberately narrow: `temporary-fixes` and
339/// `my-temp-git-notes` are real repositories somebody named badly, and refusing to track
340/// a real workspace is the worse of the two errors.
341pub const EPHEMERAL_REPO_PREFIXES: &[&str] = &["temp_git_", "tmp_git_"];
342
343/// Path fragments that mark a registry entry as an ephemeral AI-agent worktree.
344///
345/// Claude Code, Cursor, Conductor and Aider all park their disposable worktrees under a
346/// dot-directory named like `<tool>/worktrees/<branch>`, inside the repository they
347/// belong to. Those checkouts vanish when the session ends, so a registered one whose
348/// path is gone is routine churn, not a moved project: the registry heals it by dropping
349/// the entry instead of hunting for a new location.
350///
351/// Matched against a lowercased, forward-slash-normalised path, so each fragment is
352/// spelled once with `/` separators and keeps its leading and trailing slash. The
353/// anchoring is what stops a real repository named `myclaude` or `worktrees-tool` from
354/// matching.
355pub const EPHEMERAL_WORKTREE_FRAGMENTS: &[&str] = &[
356 "/.claude/worktrees/",
357 "/.cursor/worktrees/",
358 "/.conductor/worktrees/",
359 "/.aider/worktrees/",
360 "/.agents/worktrees/",
361 "/.worktrees/",
362];
363
364/// The line prefix in a linked worktree's `.git` *file* that names its metadata
365/// directory inside the main repository (`gitdir: <main>/.git/worktrees/<name>`).
366pub const GITDIR_PREFIX: &str = "gitdir:";
367
368/// Whether an adapter whose sync command edits tracked manifests may run it.
369///
370/// Off. `cargo generate-lockfile` re-resolves every dependency and rewrites
371/// `Cargo.lock`; `go mod tidy` edits `go.mod` and `go.sum` and can drop requirements.
372/// A cleanup tool that silently changes files Git tracks has done something the user
373/// did not ask for, so these run read-only and this switch is the informed opt-in.
374pub const DEFAULT_ALLOW_MANIFEST_REWRITE: bool = false;
375
376/// Whether the setup pass installs the Git hooks in front of another tool's.
377///
378/// Off. Chaining is behaviour-preserving — every hook is forwarded on and
379/// `devp hook uninstall` restores the original `core.hooksPath` — but it still rewires
380/// somebody else's Git configuration, which is not a thing to do unasked. Turn it on
381/// with `devp config set auto_hooks_chain true`, or do it once with
382/// `devp hook install --chain`.
383pub const DEFAULT_AUTO_HOOKS_CHAIN: bool = false;
384
385/// GitHub releases page, shown whenever an upgrade is relevant.
386pub const RELEASES_URL: &str = "https://github.com/Life-Experimentalist/dev-prune/releases";
387
388/// The shell installer, printed as an upgrade command and re-run by `devp update
389/// --install` when the running binary came from it.
390pub const INSTALL_SH_URL: &str = "https://devprune.vkrishna04.me/install.sh";
391
392/// The PowerShell installer — same two callers as [`INSTALL_SH_URL`].
393pub const INSTALL_PS1_URL: &str = "https://devprune.vkrishna04.me/install.ps1";
394
395/// The release page for the latest published release.
396///
397/// Contacted by `devp update` and by the interval-gated check behind
398/// `run`/`status`/`init` (off via `update_check false` or `DEV_PRUNE_OFFLINE`). See the
399/// network policy in `docs/PRIVACY.md`.
400///
401/// Deliberately the website, not `api.github.com`. GitHub redirects this page to
402/// `releases/tag/<tag>`, and the tag is read out of that `Location` header without
403/// following it — nothing else about the page is needed. The API endpoint that used to
404/// answer the same question allows an unauthenticated address sixty requests an hour
405/// and answered `403` once a busy laptop had spent them, which made `devp update` say
406/// the network was down when it was not. The website has no per-address quota.
407///
408/// Both resolve the release GitHub has marked *latest*, which is the newest binary
409/// release and nothing else: the extension's releases are published with
410/// `make_latest: false` precisely so they never surface here. They must not. The tag
411/// read from here is fed to [`compare_versions`](crate::commands::update::compare_versions)
412/// after a leading `v` is stripped, and a `vscode-v0.4.0` tag arriving here would leave
413/// every installed copy unable to compare its own version for as long as that release
414/// stayed newest.
415pub const LATEST_RELEASE_URL: &str =
416 "https://github.com/Life-Experimentalist/dev-prune/releases/latest";
417
418/// GitHub API endpoint listing releases newest-first, for the extension `.vsix`.
419///
420/// The extension ships on its own tags (`vscode-v*`) and its own release page, because
421/// its version is its own and it changes on its own schedule — see
422/// `.github/workflows/release-extension.yml`. That is also why this is a listing rather
423/// than the redirect behind [`LATEST_RELEASE_URL`]: there is no "latest release whose tag
424/// starts with" endpoint, so the caller walks the page and takes the first match.
425///
426/// One page is enough by a wide margin. The extension would have to go a hundred binary
427/// releases without a single release of its own before its newest fell off the end, and
428/// the fallback degrades to "install it by hand" rather than to anything wrong.
429pub const RELEASES_LIST_API_URL: &str =
430 "https://api.github.com/repos/Life-Experimentalist/dev-prune/releases?per_page=100";
431
432/// Tag prefix identifying a release of the VS Code extension rather than of the binary.
433pub const VSCODE_RELEASE_TAG_PREFIX: &str = "vscode-v";
434
435/// Where a release's assets live, with `{tag}` standing in for `v1.4.0`.
436///
437/// Used by `devp update --install` to fetch the uncompressed binary and its `.sha256`
438/// sidecar. Deliberately the plain `releases/download` path rather than an API endpoint:
439/// it needs no token, is not rate-limited per-IP the way `api.github.com` is, and is the
440/// same URL a person would click on the release page.
441pub const RELEASE_DOWNLOAD_BASE: &str =
442 "https://github.com/Life-Experimentalist/dev-prune/releases/download";
443
444/// Where a SHA-256 becomes a scan report, with the digest appended as the last segment.
445///
446/// `devp trust` prints one of these per executable it owns. It is a *lookup* by hash and
447/// nothing more: the digest is computed locally, the URL is printed rather than fetched,
448/// and no part of dev-prune ever uploads a file anywhere. The reason it exists is that an
449/// antivirus judges the bytes on the disk in front of it, not the asset on a release
450/// page — so the only hash worth showing someone is the one their own copy has.
451pub const VIRUSTOTAL_FILE_BASE: &str = "https://www.virustotal.com/gui/file";
452
453/// The release-asset name for one platform, without the `.sha256` suffix.
454///
455/// This is a contract with the packaging steps in `.github/workflows/release.yml`, which
456/// build these exact names. A mismatch is not a compile error and not a test failure —
457/// it is a self-update that 404s on the day of a release — so the two are commented as
458/// referring to each other.
459///
460/// `None` on a platform the release does not build for, which is how a source build on,
461/// say, FreeBSD declines the direct route instead of downloading a Linux binary.
462pub fn release_asset_name(version: &str) -> Option<String> {
463 let os = match std::env::consts::OS {
464 "windows" => "windows",
465 "macos" => "darwin",
466 "linux" => "linux",
467 _ => return None,
468 };
469 // The release publishes `x64`/`arm64`/`x86`, not Rust's target-arch spellings.
470 let arch = match std::env::consts::ARCH {
471 "x86_64" => "x64",
472 "aarch64" => "arm64",
473 "x86" => "x86",
474 _ => return None,
475 };
476 // Only Windows ships a 32-bit build; on any other OS `x86` has no asset.
477 if arch == "x86" && os != "windows" {
478 return None;
479 }
480 let ext = if os == "windows" { ".exe" } else { "" };
481 Some(format!("dev-prune-v{version}-{os}-{arch}{ext}"))
482}
483
484/// The release-asset name for the windowless scheduler binary, without the `.sha256`
485/// suffix.
486///
487/// Same contract, same workflow: the Windows packaging step in
488/// `.github/workflows/release.yml` publishes this exact name beside the console asset,
489/// so the self-update can bring `devpw.exe` forward with the same verified-download
490/// path it uses for the console binary. Windows-only because the windowless twin is:
491/// no other platform has a subsystem split to paper over.
492#[cfg(windows)]
493pub fn windowless_release_asset_name(version: &str) -> Option<String> {
494 let arch = match std::env::consts::ARCH {
495 "x86_64" => "x64",
496 "aarch64" => "arm64",
497 "x86" => "x86",
498 _ => return None,
499 };
500 Some(format!("devpw-v{version}-windows-{arch}.exe"))
501}
502
503/// Whether the periodic release check runs. On by default — see `Settings::update_check`.
504pub const DEFAULT_UPDATE_CHECK: bool = true;
505
506/// Whether a known-newer release installs itself at the end of a prune pass. On by
507/// default — see `Settings::auto_update`.
508pub const DEFAULT_AUTO_UPDATE: bool = true;
509
510/// Whether the installed version is pinned where it is. Off by default, and the only
511/// setting that outranks every other update path -- see `Settings::version_lock`.
512pub const DEFAULT_VERSION_LOCK: bool = false;
513
514/// Whether a dry run ends with the switched-off-adapter recommendations. On by
515/// default; see `Settings::recommendations`.
516pub const DEFAULT_RECOMMENDATIONS: bool = true;
517
518/// Default interval, in days, between automatic release checks.
519///
520/// A week. Frequent enough that a security fix is not missed for long, rare enough that
521/// it is invisible in day-to-day use. Override with
522/// `devp config set update_check_interval_days`.
523pub const UPDATE_CHECK_INTERVAL_DAYS: i64 = 7;
524
525/// Default timeout for the release check. Short on purpose — this is a convenience,
526/// and a user waiting on a hung socket is worse than not knowing. Override with
527/// `devp config set update_check_timeout_secs` when a proxy needs longer.
528pub const UPDATE_CHECK_TIMEOUT_SECS: u64 = 5;
529
530/// Timeout for downloading a release binary in `devp update --install`.
531///
532/// Far longer than [`UPDATE_CHECK_TIMEOUT_SECS`], which only reads a few hundred bytes
533/// of JSON: this pulls several megabytes, and a slow connection is not an error.
534pub const UPDATE_DOWNLOAD_TIMEOUT_SECS: u64 = 300;
535
536/// Name of the registry JSON file.
537pub const REGISTRY_FILENAME: &str = "registry.json";
538
539/// Config directory name under user config root.
540pub const CONFIG_DIR_NAME: &str = "dev-prune";
541
542/// Global environment variable name to override config directory location.
543pub const ENV_CONFIG_DIR_OVERRIDE: &str = "DEV_PRUNE_CONFIG_DIR";
544
545/// Prefix of the per-engine stamp a completed volume dry run leaves in the config
546/// directory (`volume-pick-docker.stamp` holds the Unix time it finished). The real
547/// `--include-volumes` pick list only arms while a stamp is fresh, so the first time
548/// anyone types the real command they get the dry run instead of a deletion prompt.
549pub const VOLUME_PICK_STAMP_PREFIX: &str = "volume-pick-";
550
551/// Extension of the volume dry-run stamp file.
552pub const VOLUME_PICK_STAMP_SUFFIX: &str = ".stamp";
553
554/// How long a completed volume dry run arms the real pick list, in seconds.
555///
556/// Long enough to read the list and retype the line; short enough that a list read
557/// this morning cannot authorize a deletion this afternoon, when the volumes on it
558/// may no longer be the unused ones.
559pub const VOLUME_PICK_WINDOW_SECS: u64 = 600;
560
561/// File in the config directory recording the first-run answer to "may dev-prune set
562/// itself up?" — `granted` or `declined`. Its absence means the question has never been
563/// asked, or that `devp uninstall` put it back that way. Separate from the version
564/// stamp on purpose: the stamp is rewritten on every upgrade, and an answer somebody
565/// gave once must survive all of them.
566pub const SETUP_CONSENT_FILE: &str = "setup-consent";
567
568/// Environment variable that suppresses the automatic setup pass entirely.
569///
570/// For images, CI and anyone who wants the binary and nothing else. `devp setup` still
571/// works when it is set — this only governs the unattended pass.
572pub const ENV_NO_AUTO_SETUP: &str = "DEV_PRUNE_NO_AUTO_SETUP";
573
574/// Environment variable that keeps the process off the network entirely — the release
575/// check and the extension-download fallback alike. Set by the test suites, useful on
576/// air-gapped machines; the durable per-user switch is
577/// `devp config set update_check false`.
578pub const ENV_OFFLINE: &str = "DEV_PRUNE_OFFLINE";
579
580/// Environment variable that stops both install scripts from asking anything.
581///
582/// The scripts offer to migrate a copy another package manager owns, and `devp install
583/// --channel installer` re-runs the script that made the offer. Without this the offer
584/// would be made again by that inner run, to a user who has already answered it — the
585/// same copy is still on PATH until the uninstall at the end. It is also the switch for
586/// a provisioning script that wants the install and none of the conversation.
587///
588/// Read by `scripts/install.sh`, `scripts/install.ps1`, and set by
589/// `src/commands/install.rs` on the child it spawns.
590pub const ENV_NO_MIGRATE_PROMPT: &str = "DEV_PRUNE_NO_MIGRATE_PROMPT";
591
592/// The install receipt, written beside the managed binary by whichever installer put it
593/// there. See [`crate::receipt`].
594///
595/// Also written by `scripts/install.sh` and `scripts/install.ps1`, by hand, in their own
596/// languages — which is why the field names have a test of their own.
597pub const INSTALL_RECEIPT_FILE: &str = "install.json";
598
599/// Set to any value to keep every full-screen view from opening, so the line-by-line
600/// fallback runs instead.
601///
602/// For agents and wrappers that hold a real terminal — the terminal test alone cannot
603/// tell them apart from a person, and a full-screen view waiting on a keypress from
604/// something that will never send one is a hang.
605pub const ENV_NO_TUI: &str = "DEV_PRUNE_NO_TUI";
606
607/// Environment variable that overrides the `language` setting for one invocation.
608///
609/// What a script or a CI job sets when it wants output in a known language whatever the
610/// machine is configured for. An unrecognised code falls back to [`DEFAULT_LANGUAGE`]
611/// rather than failing, because this is read before the command runs and a typo in a
612/// cosmetic setting should not stop a prune.
613pub const ENV_LANGUAGE: &str = "DEV_PRUNE_LANG";
614
615/// Windows environment variable that carries the *machine's* architecture when the
616/// running process is emulated.
617///
618/// `std::env::consts::ARCH` is baked in at compile time and only ever describes the
619/// binary. Windows sets this one under WOW64 and under ARM64 emulation, which is the
620/// only way a 32-bit build can tell that it is running on a 64-bit machine.
621/// `scripts/install.ps1` reads the same variable to choose which asset to download.
622pub const ENV_NATIVE_ARCH: &str = "PROCESSOR_ARCHITEW6432";
623
624/// Filename that, when present in a repo root, causes dev-prune to skip that repo entirely.
625///
626/// Create this file with: `touch ignore.devprune.json`
627pub const DEVPRUNE_IGNORE_FILE: &str = "ignore.devprune.json";
628
629/// Home-relative directory names that conventionally hold a developer's repositories.
630///
631/// Probed by name — existence is one `stat` each — when discovery has no registered
632/// repository to work outwards from. Deliberately a list of conventions rather than a
633/// walk of the home directory: `~` also contains `Library`, `AppData` and whatever a
634/// cloud-sync client has decided to materialise, and none of that is anybody's code.
635///
636/// `Documents/GitHub` is GitHub Desktop's default, `source/repos` is Visual Studio's,
637/// and `go/src` is the layout every pre-modules Go install still has.
638pub const CODE_ROOT_NAMES: &[&str] = &[
639 "Code",
640 "code",
641 "Projects",
642 "projects",
643 "Developer",
644 "Development",
645 "dev",
646 "src",
647 "repos",
648 "git",
649 "work",
650 "workspace",
651 "Documents/GitHub",
652 "source/repos",
653 "go/src",
654];
655
656/// Fewest path components a directory must have before discovery will scan it.
657///
658/// A repository cloned directly into the home directory has `~` as its parent, and
659/// "scan the parent of every registered repository" would then mean walking the whole
660/// home directory — every cache, every application-support tree, every cloud mount. The
661/// depth floor is what stops one repository in the wrong place from turning a cheap
662/// neighbourhood scan into a full-disk crawl.
663pub const MIN_DISCOVERY_ROOT_DEPTH: usize = 2;
664
665/// Default for whether the scheduled pass looks for unregistered repositories by itself.
666///
667/// On. The Git hook registers a repository the first time you commit in it, which leaves
668/// out every repository you cloned and have not committed to — exactly the idle ones
669/// worth pruning. Discovery is also the only way an assistant driving the tool learns
670/// about repositories nobody has mentioned to it.
671///
672/// Safe to have on by default because discovery only *registers*. A newly registered
673/// repository is still pruned only once it is idle past `idle_days`, only where a
674/// lockfile proves every directory recoverable, and only after the safety invariants
675/// pass — so the worst outcome of a wrong guess is a row in `devp status`.
676pub const DEFAULT_AUTO_DISCOVER: bool = true;
677
678/// Default timeout in seconds for lockfile enforcement / CLI commands (10 minutes).
679pub const DEFAULT_COMMAND_TIMEOUT_SECS: u64 = 600;
680
681/// Directory name pnpm gives a store it has to put on a volume of its own.
682///
683/// pnpm hardlinks its store into every `node_modules` it fills, and a hardlink cannot
684/// cross a filesystem. A project on a filesystem that is not the home directory's
685/// therefore gets a store at the root of *its* filesystem instead — `V:\\.pnpm-store` on
686/// a second Windows drive, `/mnt/data/.pnpm-store` on Linux, `/Volumes/Work/.pnpm-store`
687/// on macOS. `devp caches` looks for one on every volume that holds a registered
688/// repository, because `pnpm store path` only ever answers for the volume it is run on.
689pub const PNPM_VOLUME_STORE_DIR: &str = ".pnpm-store";
690
691/// Timeout for the "where does your cache live?" queries `devp caches` makes.
692///
693/// Deliberately not `command_timeout_secs`. That ceiling is sized for `npm ci` and
694/// `cargo metadata`; `npm config get cache` prints one line and returns. A query that
695/// has not answered in five seconds is a broken installation, and the report is better
696/// off falling back to the conventional path than waiting ten minutes for it.
697pub const CACHE_QUERY_TIMEOUT_SECS: u64 = 5;
698
699/// Timeout for asking a container engine how much disk it is using.
700///
701/// Longer than [`CACHE_QUERY_TIMEOUT_SECS`], because `docker system df` is not a config
702/// lookup: the daemon walks every image layer, container and build-cache record to
703/// answer it, and on a store with hundreds of images that is genuinely a few seconds. A
704/// daemon that is not running refuses in milliseconds either way, which is the case this
705/// ceiling is not for.
706pub const CONTAINER_QUERY_TIMEOUT_SECS: u64 = 20;
707
708/// Timeout for one reclaim step of `devp caches clear <engine>`.
709///
710/// An order of magnitude above the query, because deleting is not measuring. `docker
711/// image prune -a` on a 30 GB store unlinks tens of thousands of layer files, and on
712/// Docker Desktop it does that inside a VM against a virtual disk. Ten minutes is not an
713/// estimate of how long that takes; it is the point past which the daemon is stuck
714/// rather than slow, and killing the step is better than a command that never returns.
715pub const CONTAINER_PRUNE_TIMEOUT_SECS: u64 = 600;
716
717/// Ceiling for one `devp caches clear` step.
718///
719/// Ten minutes, not the five seconds a query gets: `go clean -modcache` and deleting a
720/// multi-gigabyte `~/.gradle/caches` are genuinely slow, and on Windows every file in a
721/// module cache is read-only, so the delete is a chmod-and-unlink per file. A clear that
722/// has not finished in ten minutes is wedged, and killing it leaves a partially emptied
723/// cache the manager refills on its own.
724pub const CACHE_CLEAR_TIMEOUT_SECS: u64 = 600;
725
726/// Documentation URL for troubleshooting lockfile and pruning failures.
727pub const TROUBLESHOOTING_URL: &str = "https://devprune.vkrishna04.me/docs/troubleshooting";
728/// Name of the structured per-repository configuration file stored inside repo roots.
729pub const PER_REPO_CONFIG_FILE: &str = ".devprune.json";
730
731/// Name of the committed, team-wide half of a repository's configuration.
732///
733/// Same shape and same schema as [`PER_REPO_CONFIG_FILE`], and the opposite intent.
734/// `.devprune.json` is written into `.git/info/exclude` so one person's answer stays one
735/// person's; this one is meant to be `git add`-ed, so that "nobody prunes this
736/// repository" is a fact a fresh clone already knows rather than something every
737/// teammate has to be told. The `project.` prefix rather than a new extension is what
738/// keeps one JSON schema covering both files.
739pub const PROJECT_REPO_CONFIG_FILE: &str = "project.devprune.json";
740
741/// The adapter name the declared-directory pass answers to.
742///
743/// Not a package manager and not in `get_all_adapters()`, but it appears in the same
744/// column of the same report, so it needs the same kind of name — and `--only`,
745/// `--skip` and `disabled_adapters` all match on that column. Naming it here is what
746/// keeps the filter, the engine and the report agreeing on the spelling.
747pub const DECLARED_ADAPTER_NAME: &str = "declared";
748
749/// Public URL for the JSON Schema used by IDEs for .devprune.json IntelliSense.
750pub const JSON_SCHEMA_URL: &str = "https://devprune.vkrishna04.me/schemas/v1/devprune.schema.json";
751
752/// Directory under the user's home that marks a Claude Code installation.
753///
754/// Its presence is how the setup pass decides the machine has an agent to install the
755/// skill for; the directory itself is only ever created by Claude Code.
756pub const CLAUDE_HOME_DIR: &str = ".claude";
757
758/// Subdirectory of an agent's home where Agent Skills live, one directory per skill.
759pub const AGENT_SKILLS_SUBDIR: &str = "skills";
760
761/// Marketplace identifier (`publisher.name`) of the VS Code extension, as understood
762/// by `code --install-extension`.
763pub const VSCODE_EXTENSION_ID: &str = "VKrishna04.dev-prune";
764
765/// The WinGet package identifier, as published in `packaging/winget/` and in the
766/// manifests submitted to microsoft/winget-pkgs. `devp update` and `devp uninstall` name
767/// it back to the user, so a typo here sends someone to a command that does not resolve.
768pub const WINGET_PACKAGE_ID: &str = "VKrishna04.dev-prune";
769
770/// The Homebrew tap that carries the formula, as `brew tap` takes it. Not homebrew-core:
771/// plain `brew install dev-prune` resolves there, and that has a notability bar this
772/// project has not cleared. See `docs/DISTRIBUTION.md`.
773pub const HOMEBREW_TAP: &str = "Life-Experimentalist/tap";
774
775/// The Scoop bucket name and the repository behind it. `scoop bucket add` takes both,
776/// and the name is what `scoop install` then resolves `dev-prune` against.
777pub const SCOOP_BUCKET_NAME: &str = "life-experimentalist";
778/// Repository URL for [`SCOOP_BUCKET_NAME`].
779pub const SCOOP_BUCKET_URL: &str = "https://github.com/Life-Experimentalist/scoop-bucket";
780
781/// Where a person can read about the extension before installing it.
782///
783/// The offer prints all three. The two registries carry the same build — the release
784/// `.vsix` — but a machine that trusts one may not have the other, and someone who
785/// wants to read the source before letting anything into their editor needs neither.
786pub const VSCODE_MARKETPLACE_URL: &str =
787 "https://marketplace.visualstudio.com/items?itemName=VKrishna04.dev-prune";
788/// The Open VSX listing, which is what VSCodium, Cursor and Windsurf resolve against.
789pub const OPENVSX_URL: &str = "https://open-vsx.org/extension/VKrishna04/dev-prune";
790
791/// Name of the Windows Task Scheduler task the daemon registers.
792pub const WINDOWS_TASK_NAME: &str = "DevPrune";
793/// File name of the windowless scheduler binary — the same CLI built for the GUI
794/// subsystem, the relationship `pythonw.exe` has to `python.exe`. A `[[bin]]` target, so
795/// it ships in the Windows archives and `cargo install` places it; nothing generates it
796/// on a user's machine.
797pub const WINDOWS_WINDOWLESS_BIN: &str = "devpw.exe";
798/// Marker file (in the config directory) recording that this machine's Task Scheduler
799/// refused the sessionless (S4U) task registration, so setup keeps the console task
800/// instead of retrying the upgrade on every pass.
801///
802/// Named for what the task *is* and not for what it is not: this value lands in the
803/// binary's string table a few bytes from `devpw.exe`, and "devpw" next to "hidden" is
804/// what a reviewer running `strings` reads first. Nothing here is concealed from
805/// anyone — the task is listed in Task Scheduler under its own name and the marker is
806/// an empty file in the config directory — so the vocabulary must not imply otherwise.
807pub const SCHEDULER_WINDOWLESS_REFUSED_MARKER: &str = "scheduler-windowless-refused";
808
809/// Marker recording that this machine's scheduler refused the re-registration that
810/// lifts the power gates off the task (see `windows::apply_power_settings`). Same
811/// contract as the windowless marker above: written on refusal so settled passes stop
812/// retrying, swept by the next explicit install.
813pub const SCHEDULER_POWER_REFUSED_MARKER: &str = "scheduler-power-refused";
814
815/// Scratch file the power-settings re-registration writes the patched task XML to,
816/// inside the config directory. `schtasks /Create /XML` only reads from a file.
817pub const SCHEDULER_POWER_PATCH_FILE: &str = "task-power-patch.xml";
818
819/// Label of the macOS LaunchAgent the daemon registers (also names its plist file).
820pub const MACOS_LAUNCHD_LABEL: &str = "com.devprune.daemon";
821
822/// Where `devp skill --agent cursor` writes the per-repository rules.
823pub const CURSOR_RULES_FILE: &str = ".cursor/rules/dev-prune.mdc";
824
825/// Where `devp skill --agent windsurf` writes the per-repository rules.
826pub const WINDSURF_RULES_FILE: &str = ".windsurf/rules/dev-prune.md";
827
828/// Where `devp skill --agent antigravity` writes the per-repository rules —
829/// Antigravity (Google) reads workspace rules from `.agent/rules/`.
830pub const ANTIGRAVITY_RULES_FILE: &str = ".agent/rules/dev-prune.md";
831
832/// Where `devp skill --agent cline` writes its rules (Cline reads every file in the
833/// `.clinerules/` directory).
834pub const CLINE_RULES_FILE: &str = ".clinerules/dev-prune.md";
835
836/// Where `devp skill --agent agents-md` writes its marked block — the cross-tool
837/// convention read by Codex, Jules, Amp, Antigravity and others.
838pub const AGENTS_MD_FILE: &str = "AGENTS.md";
839
840/// Where `devp skill --agent roo` writes its rules (Roo Code reads every file in
841/// `.roo/rules/`).
842pub const ROO_RULES_FILE: &str = ".roo/rules/dev-prune.md";
843
844/// Where `devp skill --agent kilocode` writes its rules (Kilo Code reads every file in
845/// `.kilocode/rules/`).
846pub const KILOCODE_RULES_FILE: &str = ".kilocode/rules/dev-prune.md";
847
848/// Where `devp skill --agent continue` writes its rules (Continue reads every file in
849/// `.continue/rules/`).
850pub const CONTINUE_RULES_FILE: &str = ".continue/rules/dev-prune.md";
851
852/// Where `devp skill --agent amazon-q` writes its rules (Amazon Q Developer reads every
853/// file in `.amazonq/rules/`).
854pub const AMAZON_Q_RULES_FILE: &str = ".amazonq/rules/dev-prune.md";
855
856/// Where `devp skill --agent kiro` writes its rules (Kiro reads every file in
857/// `.kiro/steering/`).
858pub const KIRO_STEERING_FILE: &str = ".kiro/steering/dev-prune.md";
859
860/// Where `devp skill --agent trae` writes its rules (Trae reads every file in
861/// `.trae/rules/`).
862pub const TRAE_RULES_FILE: &str = ".trae/rules/dev-prune.md";
863
864/// The shared file `devp skill --agent junie` owns a marked block inside — JetBrains
865/// Junie reads one guidelines file, not a directory.
866pub const JUNIE_GUIDELINES_FILE: &str = ".junie/guidelines.md";
867
868/// The shared file `devp skill --agent gemini` owns a marked block inside — the Gemini
869/// CLI reads one context file per repository.
870pub const GEMINI_MD_FILE: &str = "GEMINI.md";
871
872/// The shared file `devp skill --agent zed` owns a marked block inside. Zed reads
873/// `.rules` ahead of every other convention, so a repository that also has an
874/// `AGENTS.md` still needs this one.
875pub const ZED_RULES_FILE: &str = ".rules";
876
877/// The shared file `devp skill --agent aider` owns a marked block inside. Aider is the
878/// one target that does not read its file on its own: `CONVENTIONS.md` is loaded only
879/// by `aider --read CONVENTIONS.md` or a `read: CONVENTIONS.md` line in
880/// `.aider.conf.yml`, which is why writing it prints that instruction.
881pub const AIDER_CONVENTIONS_FILE: &str = "CONVENTIONS.md";
882
883/// The shared file `devp skill --agent copilot` owns a marked block inside.
884pub const COPILOT_INSTRUCTIONS_FILE: &str = ".github/copilot-instructions.md";
885
886/// Markers around the block in [`COPILOT_INSTRUCTIONS_FILE`] that dev-prune manages.
887/// Everything outside them belongs to the user and is never touched.
888pub const RULES_BLOCK_START: &str = "<!-- dev-prune:rules:start -->";
889pub const RULES_BLOCK_END: &str = "<!-- dev-prune:rules:end -->";
890
891/// Windows clipboard command that `devp skill --copy` shells out to. Also reachable
892/// from WSL, since `clip.exe` sits on PATH there too.
893pub const CLIPBOARD_COMMAND_WINDOWS: &str = "clip.exe";
894
895/// macOS clipboard command that `devp skill --copy` shells out to.
896pub const CLIPBOARD_COMMAND_MACOS: &str = "pbcopy";
897
898/// Linux clipboard commands `devp skill --copy` tries in order, before falling back to
899/// [`CLIPBOARD_COMMAND_WINDOWS`] for WSL. Each pairs the binary with the arguments that
900/// target the clipboard proper rather than the X11 primary-selection buffer.
901pub const CLIPBOARD_COMMANDS_LINUX: &[(&str, &[&str])] = &[
902 ("wl-copy", &[]),
903 ("xclip", &["-selection", "clipboard"]),
904 ("xsel", &["--clipboard", "--input"]),
905];
906
907/// The phrase Git uses when it refuses a working tree owned by another account.
908///
909/// Matched against Git's own stderr rather than parsed: the message is twelve lines
910/// long, ten of which are identical for every repository refused, and `devp run`
911/// groups every repository sharing this cause under one explanation and one fix
912/// instead of reprinting those ten lines per repository.
913pub const GIT_DUBIOUS_OWNERSHIP: &str = "detected dubious ownership";
914
915/// The phrase Git uses when the path it was pointed at is not a working tree at all.
916///
917/// Same reasoning as [`GIT_DUBIOUS_OWNERSHIP`]: one cause, one fix, one paragraph.
918pub const GIT_NOT_A_REPOSITORY: &str = "not a git repository";
919
920/// Output directory graphify writes at the root of the folder it was pointed at, per
921/// its own `SKILL.md`. There is no lockfile that proves it can be rebuilt: doing so
922/// re-runs every extraction pass, LLM calls included, so this is reported under "Tools"
923/// and never a prune candidate.
924pub const GRAPHIFY_OUTPUT_DIR: &str = "graphify-out";
925
926/// Output directory understand-anything writes at the root of the project it scanned
927/// (`UA_DIR` in its own `persistence/index.ts`). Same reasoning as
928/// [`GRAPHIFY_OUTPUT_DIR`]: rebuilding it costs LLM calls, so it is reported, not pruned.
929pub const UNDERSTAND_ANYTHING_DIR: &str = ".understand-anything";