Skip to main content

dev_prune/
help.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// The long-form help text: what `devp <command> --help` prints.
5//
6// `-h` stays short — one line per flag, no scrolling. `--help` (and `devp help
7// <command>`) gets these: the full description, the behaviour that is not obvious from
8// the flag list, and worked examples for every command and subcommand. The text is
9// kept in one file rather than inline in the derive so the CLI definition in `lib.rs`
10// stays readable, and because these paragraphs are documentation first — they carry
11// the same facts as `docs/CLI_REFERENCE.md`, and changing one means changing both.
12
13pub const INIT_LONG: &str = "\
14Crawl the given directory trees for Git repositories and register every one found, up \
15to 8 levels deep. Registration is one entry in dev-prune's own registry file — nothing \
16in the repository is created, changed or deleted.
17
18After registering, it runs the same integration pass as `devp setup` (installing \
19whatever is missing, reporting whatever it skipped) and the same quiet release check \
20as `devp update`.
21
22`--auto` works the paths out instead of being told them: the directories your \
23registered repositories already sit in, the workspace you are standing in, and the \
24conventional locations under your home directory. It is the form to use when nobody \
25has said where the code is — an assistant setting the tool up, or a machine whose \
26repositories you would rather not list by hand.
27
28A bulk scan skips any repository holding an `ignore.devprune.json`, so a repository can \
29decline before it is ever registered. `devp link <path>` still registers it, because \
30naming one repository is not a bulk scan.
31
32Registration is what makes a repository visible to `devp run`, `devp status` and the \
33background pass. Registering is not pruning: a registered repository is still only \
34touched once it is idle, and only where a lockfile proves the directory can be rebuilt.";
35
36pub const INIT_EXAMPLES: &str = "\
37EXAMPLES:
38  devp init                       Register repositories under the current directory
39  devp init --auto                Work out where the repositories are and register them
40  devp init --auto --dry-run      Show what that would register, write nothing
41  devp init ~/Code                Register everything under ~/Code
42  devp init ~/Code ~/Work/oss     Multiple trees in one pass
43  devp scan ~/Code                Same command — `scan` and `onboard` are aliases
44  DEV_PRUNE_NO_AUTO_SETUP=1 devp init ~/Code
45                                  Register repositories, install nothing
46
47OPTING OUT:
48  ignore.devprune.json            A file by that name in a repository keeps it out,
49                                  both of a bulk scan and of every prune pass
50
51UNDO:
52  devp undo                       Reverts the most recent init or link";
53
54pub const LINK_LONG: &str = "\
55Register one Git repository for pruning. The path defaults to `.`, so inside a \
56repository `devp link` is the whole command. Registration writes one entry to \
57dev-prune's registry; the repository itself is untouched.
58
59`--quiet` is the form the global Git hook invokes: it prints nothing (a hook fires \
60inside someone's commit, whose terminal is not dev-prune's to write to) and it skips \
61repositories whose `.devprune.json` sets `disable_hooks`, so a workspace that opted \
62out of auto-registration stays out.";
63
64pub const LINK_EXAMPLES: &str = "\
65EXAMPLES:
66  devp link                       Register the current directory
67  devp link ~/Code/my-app         Register a repository by path
68  devp link . --quiet             What the Git hook runs; silent, honours opt-outs
69
70UNDO:
71  devp undo                       Reverts the most recent init or link
72  devp unlink                     Unregister (keeps every file on disk)";
73
74pub const UNLINK_LONG: &str = "\
75Remove a repository from dev-prune's registry. This deletes the registry entry and \
76nothing else — no workspace file is touched, and the repository's `.devprune.json`, \
77if it has one, stays where it is.
78
79`--missing` removes every registered path whose directory no longer exists, instead \
80of one named repository. Deleted clones, reformatted drives and moved workspaces all \
81leave dead entries behind; `devp doctor` counts them in one warning and points here \
82rather than printing one `devp unlink` line per dead path.";
83
84pub const UNLINK_EXAMPLES: &str = "\
85EXAMPLES:
86  devp unlink                     Unregister the current directory
87  devp unlink ~/Code/old-app      Unregister a repository by path
88  devp unlink --missing           Drop every entry whose path no longer exists";
89
90pub const UNDO_LONG: &str = "\
91Revert the most recent `devp init` or `devp link`: whatever repositories that one \
92action registered are unregistered again. Only registration is undone — `undo` never \
93deletes files, and it is not the undo for a prune (that is `devp restore --last-run`).";
94
95pub const UNDO_EXAMPLES: &str = "\
96EXAMPLES:
97  devp undo                       Unregister whatever the last init/link registered
98
99RELATED:
100  devp restore --last-run         The undo for a prune pass — reinstalls what it deleted";
101
102pub const RUN_LONG: &str = "\
103Execute a prune pass: across every registered repository with no path, or on one \
104repository with `devp run <path>`. Each repository goes through the same gauntlet, \
105and a directory is deleted only when every check passes:
106
107  1. `ignore.devprune.json` in the root, or `\"ignore\": true` — skipped instantly.
108  2. Idle check: last commit and newest source mtime, against `idle_days` (15 by
109     default). `--ignore-idle` lifts this one check and nothing else.
110  3. Project discovery: the root and up to `scan_depth` levels below it (6 by
111     default), so a monorepo's every package is found.
112  4. The package manager's own binary must be present — a directory whose manager
113     is missing cannot be verified, so it is not touched.
114  5. Lockfile verification: the manager itself confirms the lockfile can rebuild
115     the directory. No flag bypasses this, and none ever will.
116  6. Size floor (`min_size_mb` / `--min-size`), symlink refusal, nested-repository
117     refusal.
118
119Interactively, a registry-wide pass opens a selection TUI showing what would be \
120deleted before anything is; a targeted `devp run <path>` lists every directory with \
121its size and the total, notes that `devp restore` brings it back, and asks. Either \
122way `-y` skips the confirmation, `--dry-run` reports what a pass would do without \
123deleting anything at all, and without a terminal the run exits with an error naming \
124`--yes` rather than waiting on a prompt. Adapter names for `--only`/`--skip` are: npm, pnpm, yarn, bun, uv, \
125poetry, pdm, pipenv, venv, cargo, go, composer, bundler, cocoapods, mix, mix_build, \
126gradle, maven, swift, terraform, dart, vcpkg, cmake_build — an unknown name is an \
127error listing the valid ones, not a silently empty pass. cargo, gradle, maven, \
128swift, dart, mix_build, vcpkg and cmake_build are opt-in (`devp config set \
129enable_cargo true`) and idle-gated separately by `build_idle_days`, because a \
130build directory takes far longer to get back than a dependency directory. \
131`devp config wizard` switches them on by language, and can give any one adapter \
132its own idle window.
133
134`--except` is the safe spelling of \"clean up but keep the API project\": the named \
135repositories are never verified, never deleted and never restored, which beats \
136pruning them and downloading everything back. Entries match by full path or by \
137directory name, case-insensitively, `~` expanded.
138
139`--explain` answers \"why was that repository not pruned?\": every repository and \
140directory is listed with its verdict, including the states a normal pass keeps quiet \
141about — still active (with the actual age), opted out, under the size floor. It is \
142read-only and cannot be combined with `--json`.";
143
144pub const RUN_EXAMPLES: &str = "\
145EXAMPLES:
146  devp run --dry-run              What would be pruned, and why the rest would not
147  devp run                        Prune across all registered repositories (asks first)
148  devp run -y                     Same, no confirmation prompt
149  devp run .                      Prune only the current repository (asks first)
150  devp run ~/Code/my-app --ignore-idle -y
151                                  Prune it even though it was touched recently
152  devp run --except api-service,~/Code/playground
153                                  Everything except the ones you name
154  devp run --only cargo,uv --dry-run
155                                  Only these package managers
156  devp run --skip venv --min-size 50
157                                  Skip venvs; ignore directories under 50 MiB
158  devp run --json --dry-run       One JSON document on stdout (schema in CLI_REFERENCE)
159  devp run --explain              Why each repository would or would not be pruned
160
161Run interactively with a terminal, `--json` also copies the document to the clipboard.
162A run in which any repository failed exits 1, even if others succeeded.";
163
164pub const STATUS_LONG: &str = "\
165The dashboard: every registered repository with its state (Candidate, Active, \
166Ignored, No Bloat, Path Missing, or an unreadable `.devprune.json`), its reclaimable \
167space, and its last activity. In a terminal this is an interactive TUI; piped or \
168redirected it prints a plain table; `--json` replaces either with one document and \
169changes nothing at all.
170
171Sizes are what deleting the directory actually gives back: bytes hardlinked into a \
172pnpm or bun store are measured per file and excluded, because the store keeps them.
173
174TUI KEYS:
175  Up/Down, j/k      Move           PgUp/PgDn        Jump ten rows
176  Home/End, g/G     First/last     p                Prune-select mode (candidates pre-selected)
177  Space             Toggle row     a                Toggle all candidates
178  Enter             Prune the selection             i    Toggle ignore in .devprune.json
179  Esc               Leave mode / exit               q, Ctrl-C   Exit
180
181SHORTCUTS:
182  devp status daemon              = devp config daemon status
183  devp status . hook              = devp config hook . status";
184
185pub const STATUS_EXAMPLES: &str = "\
186EXAMPLES:
187  devp status                     The dashboard (TUI in a terminal, table when piped)
188  devp status --top 10            Only the ten biggest repositories; totals unaffected
189  devp status --drift             What would a prune refuse on, and how to record it
190  devp status --json | jq '.totals.reclaimable_bytes'
191                                  Machine-readable; stdout carries the document only
192  devp status --top 5 --json      The trim is reported as a top-level \"top\" field
193
194Run interactively with a terminal, `--json` also copies the document to the clipboard.";
195
196pub const STATS_LONG: &str = "\
197What dev-prune has already done, as opposed to what it could do next: lifetime space \
198reclaimed, how many prune passes there have been, the most recent pass and how to \
199undo it, the last passes, and the repositories that have given back the most. \
200Read-only — it reads the registry and touches nothing.";
201
202pub const STATS_EXAMPLES: &str = "\
203EXAMPLES:
204  devp stats                      The report
205  devp stats --json | jq '.lifetime.bytes_freed'
206                                  Lifetime bytes as a number
207
208Run interactively with a terminal, `--json` also copies the document to the clipboard.";
209
210pub const HISTORY_LONG: &str = "Which pass deleted what. `devp stats` counts the passes and adds up the bytes; this lists them, newest first, and `--pass 1` opens one up: when it ran, whether someone typed it or the scheduler started it, the exact command with its flags, and every directory it removed grouped under the repository it came from.
211
212The list is one line per pass on purpose. A pass across forty repositories has hundreds of directories in it, so the detail is asked for by number rather than printed by default, and `--export` writes the whole log to a file for the times when the answer really is all of it. Redirect or pipe `--pass N` and nothing is elided — only a terminal gets a cap, because only a terminal has a scrollback to lose it in.
213
214Per-directory detail is recorded from 1.17.0 onward. Passes older than that are still listed, from the totals the registry has always kept, and marked so an upgraded machine does not look like it lost them.
215
216Read-only. It reads the log and the registry and writes nothing, except the file `--export` is asked for.";
217
218pub const HISTORY_EXAMPLES: &str = "EXAMPLES:
219  devp history                    Every pass, one line each, newest first
220  devp history --all              Past the 20 the list stops at
221  devp history --pass 1           What the most recent pass deleted, and what asked it to
222  devp history --pass 3 --json    That pass as one JSON document
223  devp history --export           Write the whole log to your documents folder
224  devp history --export ./log.json
225                                  Write it where you say instead
226  devp history --json | jq '[.passes[] | select(.trigger == \"scheduled\")]'
227                                  Only the passes nobody typed
228
229Run interactively with a terminal, `--json` also copies the document to the clipboard.";
230
231pub const MAN_LONG: &str = "Render the manual, from the same clap definitions `--help` prints, so the manual cannot describe a flag the program does not have.
232
233`devp man` at a terminal prints the contents page: every command grouped by what it is for, one line each, plus the flags that go before the command and the exit codes. `devp man <command>` prints that one command's page — the same text `devp <command> --help` prints, because they are the same definition.
234
235The roff source is something `man` formats, not something a person reads, and on Windows there is no `man` to hand it to, so it appears only where something can use it: redirect or pipe the output and it is roff again, so `devp man > devp.1` and `devp man | man -l -` are unchanged. `--roff` forces roff at a terminal too.
236
237`--dir` writes the full set (`devp.1`, `dev-prune.1`, and one `devp-<command>.1` per subcommand) into a directory, ready to copy onto `manpath`.";
238
239pub const MAN_EXAMPLES: &str = "EXAMPLES:
240  devp man                        The contents page, on any platform
241  devp man run                    One command's page
242  devp man | man -l -             Read it formatted by man (Linux/macOS)
243  devp man --roff > devp.1        Save the roff source
244  devp man run --roff > devp-run.1   Save one page's roff source
245  devp man --dir ./man            Write the full set into ./man
246  sudo cp man/*.1 /usr/local/share/man/man1/   Install them system-wide";
247
248pub const COMPLETIONS_LONG: &str = "\
249Print a shell completion script to stdout — the script and nothing else, because the \
250output is meant to be redirected or eval'd and anything extra becomes a shell error \
251on every new terminal.
252
253The script is generated from the same argument definition the binary parses with, so \
254a flag cannot exist in one and be missing from the other. It completes whichever name \
255invoked it: `devp completions bash` completes `devp`, `dev-prune completions bash` \
256completes `dev-prune` — generate one for each name you actually type.";
257
258pub const COMPLETIONS_EXAMPLES: &str = "\
259EXAMPLES:
260  source <(devp completions bash)                       Bash, current shell
261  devp completions bash > ~/.local/share/bash-completion/completions/devp
262  devp completions zsh  > ~/.zfunc/_devp                Zsh (a directory on $fpath)
263  devp completions fish > ~/.config/fish/completions/devp.fish
264  devp completions powershell | Out-File -Append -Encoding utf8 $PROFILE
265                                                        PowerShell, permanently";
266
267pub const CACHES_LONG: &str = "\
268Find every package-manager cache and store on the machine, size each one, and print \
269the command that clears it — largest first, with a total. On its own it deletes \
270nothing, and nothing that runs on a schedule ever will: a cache is shared by every \
271repository, so no single lockfile can prove its contents recoverable, which is the \
272bar every dev-prune deletion has to clear. Clearing one also turns the next `devp \
273restore` into a download.
274
275`devp caches clear <manager>` runs the command this table prints, after showing you \
276what goes and asking.
277
278`--volume V:` (or `--drive`, or `--volume /mnt/data`, or any path on the one you \
279mean) narrows the whole report to the caches that sit on one drive, and the \
280unfiltered table gains a line breaking the total down the same way. On a machine \
281whose projects live on a second disk, 22 GiB of caches is not the figure that \
282decides anything — the two gigabytes on the drive that is full is.
283
284Covered: npm, pnpm, yarn, bun, uv, pip, conda, cargo, go, maven, gradle, nuget, vcpkg, \
285conan, composer, cocoapods and hex. Each manager is asked where its cache is (`npm \
286config get cache`, `go env GOMODCACHE`, …) rather than assumed, with read-only queries \
287run from your home directory; a manager that is not installed falls back to the \
288conventional location, because a cache left behind by an uninstalled manager is \
289exactly the multi-gigabyte directory nobody remembers.";
290
291pub const CACHES_EXAMPLES: &str = "\
292EXAMPLES:
293  devp caches                     The table, largest first, with clear commands
294  devp caches --json | jq '.summary.total_bytes'
295                                  Machine-readable
296  devp caches clear npm           Empty one, after asking
297  devp caches clear all --dry-run What would go, and nothing touched
298  devp caches --volume V:         Only what sits on that drive (--drive is the same)
299
300Run interactively with a terminal, `--json` also copies the document to the clipboard.";
301
302pub const CACHES_DOCKER_LONG: &str = "\
303What the engine is holding, in its own words: images, containers, local volumes and build cache, each with a count, a size, and how much of that size it believes it could give back. Then the commands that would give it back, narrowest first.
304
305Read-only, permanently. dev-prune deletes only what a lockfile proves it can rebuild, and nothing here clears that bar: an image's registry tag can be retagged or deleted, the Dockerfile that built it may not be on this disk, and a named volume is the one thing on the machine that is not reproducible at all. So this prints the prune commands and never runs them, with or without `--yes`.
306
307The numbers come from the engine's own `system df` rather than from a directory walk. On Docker Desktop and Podman the store lives inside a VM disk image the host cannot see, and `~/.docker` is configuration rather than data — a size taken off the filesystem would be wrong by orders of magnitude, in the reassuring direction. Asking the engine is also the only way to learn what is *reclaimable*, which is the figure that decides anything: 40 GB of images with 38 GB dangling is a different situation from 40 GB with 2 GB dangling.
308
309An engine that is installed with its daemon stopped is reported as exactly that, in the engine's own words, rather than as an absence.";
310
311pub const CACHES_CONTAINERS_LONG: &str = "\
312The same read-only report as `devp caches docker`, for every container engine on this machine — docker, podman, nerdctl, finch and Apple's `container` — or for the one you name. An engine that is not installed is not mentioned; there is nothing to say about a tool that is not there.
313
314Local Kubernetes clusters are listed by name and deliberately not sized. kind, k3d and minikube run their nodes as containers, or as a VM disk belonging to an engine already in the table, so their disk is counted there. A figure beside the cluster name would be the same gigabytes twice. Delete one with its own tool — `kind delete cluster`, `minikube delete`, `k3d cluster delete` — which is what actually releases the space. The cluster list is read out of your kubeconfig with `kubectl config get-contexts`, which contacts nothing: a context pointing at a production cluster is filtered out by name here rather than by being dialled.";
315
316pub const CACHES_CONTAINERS_EXAMPLES: &str = "\
317EXAMPLES:
318  devp caches docker                Images, containers, volumes, build cache
319  devp caches podman                The same, for Podman
320  devp caches containers            Every engine installed, plus local clusters
321  devp caches containers nerdctl    Just that one
322  devp caches containers container  Apple's engine, on Apple silicon
323  devp caches docker --json | jq '.summary.reclaimable_bytes'
324                                    Machine-readable
325
326Nothing here deletes anything. The prune commands are printed for you to run.";
327
328pub const CACHES_CLEAR_LONG: &str = "\
329Empty one manager's cache, or every one of them. What is about to go is listed and \
330sized first, and unless `--yes` answers for you, it asks.
331
332This is a convenience, not automation. No scheduler, no Git hook and no `devp run` \
333will ever clear a cache — this only runs when you type it.
334
335Wherever the manager ships its own subcommand, that is what runs: `npm cache clean \
336--force`, `pnpm store prune`, `go clean -modcache`. The manager knows what is still \
337referenced, which a directory delete cannot work out, and its own bookkeeping stays \
338consistent. cargo, gradle, vcpkg and hex ship nothing equivalent, so those \
339are cleared by removing the directory this command resolved and sized — never a string \
340handed to a shell.
341
342Maven is reported and never cleared. `~/.m2/repository` is an install target as \
343well as a download cache — `mvn install:install-file` puts artifacts there that no \
344remote can hand back — so dev-prune sizes it and prints `rm -rf ~/.m2/repository` \
345for you to run. `clear maven` says so and stops; `clear all` skips it.
346
347The target takes a list: `devp caches clear npm,uv,pip` empties exactly those three \
348and nothing else — a one-time whitelist, no configuration involved. The mirror image \
349is `devp caches clear all --except npm,uv`: everything goes except the names given, \
350for the day one cache is the only one worth keeping warm. `--except` only makes sense \
351with `all` — a list already says exactly what to clear — and a container engine never \
352appears in either: naming an engine alone is the consent to touch it.
353
354Two flags narrow what `all` means, so you do not have to pick the caches by hand. \
355`--over-cap` keeps only the managers that have outgrown the ceiling you set in \
356`cache_max_gb`; with no cap set anywhere it clears nothing and says so. `--unused` keeps \
357only the managers that no registered repository uses — a cache with nothing behind it \
358was filled for projects that are not on this disk any more. It counts only repositories \
359dev-prune knows about, so `devp link` anything you keep outside the registry first, and \
360it refuses to run at all when there are no registered repositories to check against.
361
362Nothing else in a cache is lost; every manager re-downloads what it needs. What it costs \
363is time, in every project on the machine, on the next install and the next `devp \
364restore`. The freed size reported afterwards is measured rather than assumed, because \
365a `prune` keeps what is still in use.";
366
367pub const CACHES_CLEAR_EXAMPLES: &str = "\
368EXAMPLES:
369  devp caches clear npm           One manager, after confirming
370  devp caches clear cargo         Both cargo rows: the registry cache and its sources
371  devp caches clear npm,uv,pip    Just these three — a one-time whitelist
372  devp caches clear all --except npm,uv
373                                  Everything but these — a one-time blacklist
374  devp caches clear all --dry-run Everything that would go, and nothing touched
375  devp caches clear all --over-cap
376                                  Only the ones past their cache_max_gb
377  devp caches clear all --unused  Only the ones no registered repository uses
378  devp caches clear all --yes     No prompt, for a script
379  devp caches clear go --json --yes
380                                  Machine-readable (`--json` requires `--yes`)
381
382Exit code 1 if any cache could not be cleared; the rows are printed either way.
383Exit code 2 for `maven`, which is reported but never cleared.";
384
385pub const TRUST_LONG: &str = "\
386What dev-prune is allowed to do on this machine, on one screen. Read-only — it reads \
387the registry and the OS and changes nothing.
388
389Two sections, and the split is the point. The first is guaranteed by the code: the \
390seven safety invariants plus the two questions asked as often as any of them — there \
391is no telemetry endpoint, and build output is never deleted. Those rows read the same \
392on every machine and have no setting and no flag behind them. The second is read live \
393off this machine: whether the scheduler is installed, whether the Git hooks register \
394repositories on their own, how many repositories are registered, and the settings that \
395widen what may happen without you asking for it.
396
397There is no letter grade. A report that says `trust level: MEDIUM` tells you nothing \
398you can act on, so the widened settings are named instead — `devp config show` has \
399every one of them, and `devp config set <key> <value>` puts one back.
400
401The long form of the guarantees is docs/SAFETY_INVARIANTS.md.";
402
403pub const TRUST_EXAMPLES: &str = "\
404EXAMPLES:
405  devp trust                      The report
406  devp trust --json | jq '.summary.widened'
407                                  Just the settings that widen what may happen
408  devp trust --json | jq -e '.summary.widened_count == 0'
409                                  Exit 1 from jq if this machine has widened anything
410
411Run interactively with a terminal, `--json` also copies the document to the clipboard.";
412
413pub const CONFIG_LONG: &str = "\
414Everything configurable lives under here: global settings (get/set/show/wizard), the \
415per-repository `.devprune.json` (project), the OS background scheduler (daemon), the \
416global Git auto-registration hooks (hook), and the file-manager icon registration \
417(icon).
418
419SHORTHANDS — `daemon`, `hook` and `icon` work without the leading `config`, and the \
420action words people reach for are accepted:
421  devp hook install       = devp config hook enable
422  devp hook uninstall     = devp config hook disable
423  devp daemon on / off    = devp config daemon enable / disable
424  devp icon               = devp config icon
425Accepted action words: enable/install/on, disable/uninstall/remove/off, status/show. \
426Anything else is rejected — a mistyped action never silently degrades into a status \
427report.";
428
429pub const CONFIG_EXAMPLES: &str = "\
430EXAMPLES:
431  devp config show                Every global setting and its value
432  devp config get idle_days       One setting
433  devp config set idle_days 30    Change it (rejects out-of-range values)
434  devp config recommended         Turn on everything the first run recommends
435  devp config wizard              Walk through every setting, Enter keeps the current
436  devp config project .           Inspect or create this repo's .devprune.json
437  devp config daemon status       Is the background pass scheduled?
438  devp config . daemon disable    Opt this repository out of the background pass
439  devp config hook enable --chain Take core.hooksPath, forwarding to the tool holding it
440  devp config icon                Register the .devprune.json icon and schema";
441
442pub const CONFIG_GET_LONG: &str = "\
443Print one global setting's current value. The keys, defaults and meanings:
444
445  idle_days                  15     Days untouched before a repo is a candidate
446  min_size_mb                0      Smallest directory worth deleting (0 = no floor)
447  scan_depth                 6      Directory levels below a repo root discovery descends (1-32)
448  require_confirmation       true   Whether a prune pass asks before deleting
449  allow_manifest_rewrite     false  Whether verification may repair a drifted lockfile
450  command_timeout_secs       600    Ceiling on any one package-manager command
451  auto_setup                 true   Whether the integration pass may run unattended
452  auto_daemon                true   …and may register the OS scheduler
453  check_interval_days        2      How often the scheduler runs a pass
454  auto_hooks                 true   …and may install the global Git hooks
455  auto_hooks_chain           false  …and may chain onto another tool's core.hooksPath
456  update_check               true   Whether the periodic release check runs
457  update_check_interval_days 7      Minimum gap between two release checks
458  update_check_timeout_secs  5      How long that one request may hang
459
460Three have a per-repository override in `.devprune.json`, where they win for that \
461tree only: `idle_days` (spelled `override_idle_days` there), `min_size_mb` and \
462`scan_depth`. The rest are deliberately global — a committed `.devprune.json` must \
463not be able to grant a repository `allow_manifest_rewrite` over its own manifests.";
464
465pub const CONFIG_GET_EXAMPLES: &str = "\
466EXAMPLES:
467  devp config get idle_days
468  devp config get update_check";
469
470pub const CONFIG_SET_LONG: &str = "\
471Change one global setting. A value outside the accepted range is rejected with the \
472range in the message, never silently clamped — `scan_depth 0` and `scan_depth 40` are \
473both refused outright. Keys are the same list `devp config get --help` shows.";
474
475pub const CONFIG_SET_EXAMPLES: &str = "\
476EXAMPLES:
477  devp config set idle_days 30
478  devp config set min_size_mb 50
479  devp config set command_timeout_secs 1200
480  devp config set update_check false    Turn the release check off for good";
481
482pub const CONFIG_SHOW_LONG: &str = "\
483Print every global setting with its current value. With `--update`, also run a sync \
484pass across all registered repositories, refreshing each one's `.devprune.json` \
485scaffolding without touching values you have changed.";
486
487pub const CONFIG_SHOW_EXAMPLES: &str = "\
488EXAMPLES:
489  devp config show
490  devp config show --update";
491
492pub const CONFIG_PROJECT_LONG: &str = "\
493Inspect a repository's `.devprune.json`, or create it when missing. The file holds \
494the per-repository overrides — `override_idle_days`, `min_size_mb`, `scan_depth`, \
495`ignore`, `disable_daemon`, `disable_hooks` — and carries a `$schema` line so any \
496editor with JSON Schema support validates and completes it.
497
498`--update` refreshes the file's scaffolding (schema pointer, missing keys) while \
499keeping every value you have set. A file that does not parse is refused, not reset — \
500fix it, or pass `--update` deliberately.
501
502Writing the file also records it in the repository's `.git/info/exclude`, so the \
503config — one machine's preference, not part of the project — never shows up in \
504`git status`. The shared, tracked `.gitignore` is never modified.
505
506`--team` addresses `project.devprune.json` instead: same keys, same schema, and \
507deliberately not excluded, because it is the half meant to be committed. Every key it \
508names wins over `.devprune.json`; every key it leaves out is still yours to answer. It \
509is created empty apart from the schema line for that reason. Nothing dev-prune writes \
510on your behalf ever edits it.
511
512Both files can also carry `prunable.directories`: directories no lockfile describes, \
513each with the `rebuild` command that puts it back. Unlike every other key, the two \
514files' lists add up rather than one winning — a team declaration never discards your \
515own. Before deleting one, dev-prune checks that it is inside the repository, that Git \
516is tracking nothing in it, and that the rebuild command's tool is on this machine.
517
518`prunable.exclude` lists declared paths to leave alone on this machine, whoever \
519declared them — how you keep a directory the committed file calls rebuildable without \
520editing a file the whole team shares. Spelled the same way as a `path`, and honoured \
521from whichever file names it, because a veto only ever deletes less. Naming one path in \
522both lists of the *same* file is a typo rather than a decision — the exclusion still \
523wins, so the declaration never runs — and `devp doctor` says so.
524
525Both files are read from the repository root and nowhere else, because the paths inside \
526them are relative to that root. A copy one directory down parses and does nothing at \
527all; `devp doctor` names it rather than moving it, since moving it would change what \
528every path inside it means.";
529
530pub const CONFIG_PROJECT_EXAMPLES: &str = "\
531EXAMPLES:
532  devp config project .           Show (or create) this repository's config
533  devp config project ~/Code/api
534  devp config project . --update  Refresh scaffolding, keep your values
535  devp config project . --team    Create the committed project.devprune.json";
536
537pub const CONFIG_DAEMON_LONG: &str = "\
538The OS background scheduler — Task Scheduler on Windows, launchd on macOS, systemd \
539timers on Linux — which runs `devp run --daemon` every `check_interval_days` days.
540
541Globally: `enable` registers the schedule, `disable` removes it, `status` reports \
542it. With a path first, the same words act on one repository via `disable_daemon` in \
543its `.devprune.json`: the machine-wide pass keeps running, that repository sits it \
544out.";
545
546pub const CONFIG_DAEMON_EXAMPLES: &str = "\
547EXAMPLES:
548  devp daemon status              Machine-wide scheduler state
549  devp daemon enable              Register the schedule (also: install, on)
550  devp daemon disable             Remove it (also: uninstall, remove, off)
551  devp config . daemon disable    This repository opts out of the background pass
552  devp config ~/Code/api daemon enable";
553
554pub const CONFIG_HOOK_LONG: &str = "\
555The global Git hooks (via `core.hooksPath`) that auto-register any repository you \
556commit in — `devp link --quiet`, silent, honouring opt-outs. `enable` installs them, \
557`disable` removes them (restoring what was there), `status` reports them. With a \
558path first, the same words act on one repository via `disable_hooks`.
559
560Git has exactly one global `core.hooksPath` and no way to chain two, so a tool that \
561holds it (husky, pre-commit, lefthook) shuts every other one out. `--chain` is the \
562way through: dev-prune takes the slot and writes, per hook, a shim that does its own \
563work and then execs the same-named hook in the displaced directory — their hooks \
564keep firing, their exit codes still block commits. `devp hook uninstall` puts the \
565original back. The chain snapshots the other tool's hooks at install time; `devp \
566hook status` reports any that have drifted, and `devp hook install --chain` rebuilds.";
567
568pub const CONFIG_HOOK_EXAMPLES: &str = "\
569EXAMPLES:
570  devp hook status                Installed? Chained? Drifted?
571  devp hook install               Take the free core.hooksPath slot
572  devp hook install --chain       Take a slot husky/pre-commit/lefthook holds, forwarding
573  devp hook uninstall             Restore the previous core.hooksPath
574  devp config . hook disable      This repository opts out of auto-registration";
575
576pub const CONFIG_ICON_LONG: &str = "\
577Register `*.devprune.json` with the OS file manager and write the icon files and the \
578JSON Schema into the config directory. On Linux this is a complete registration \
579(shared-mime-info plus hicolor icons — Nautilus, Dolphin, Thunar, Nemo, PCManFM). On \
580Windows, Explorer resolves icons by last extension only, so the config folder gets \
581its own icon instead of hijacking every `.json` on the machine. On macOS a UTI must \
582come from an application bundle, which a single binary is not.
583
584It also prints an editor snippet to paste yourself — it never edits your editor \
585settings, your PATH, or your shell startup files.";
586
587pub const CONFIG_ICON_EXAMPLES: &str = "\
588EXAMPLES:
589  devp icon                       Same command, without the leading `config`";
590
591pub const CONFIG_RECOMMENDED_LONG: &str = "\
592Turn on everything the first run recommends, without sitting through the first run.
593
594The recommendations are the adapters and behaviours that are off by default because \
595they are not universally wanted, not because they are risky: Cargo, Gradle, Maven, \
596Swift, Dart, Mix builds, vcpkg and CMake builds. Accepting them all is one command \
597here and one keypress in `devp config wizard`, and both read the same list, so the \
598two can never drift apart.
599
600One recommendation is held back: `allow_manifest_rewrite` lets `cargo` and `go` tidy \
601up their own manifests during a restore, which edits files in your working tree. \
602That is worth having and it is worth knowing about first, so it arrives only when \
603you type --with-cautious. Everything printed is also printed by `devp config show`, \
604which lists whatever you have not taken yet.
605
606Nothing here is irreversible: `devp config set <key> false` puts any of it back, and \
607this command never marks the settings as reviewed — the walkthrough you skipped is \
608still owed to you, and will still open.";
609
610pub const CONFIG_RECOMMENDED_EXAMPLES: &str = "\
611EXAMPLES:
612  devp config recommended                  Everything recommended without a caveat
613  devp config recommended --with-cautious  That, plus allow_manifest_rewrite
614  devp config show                         What is still outstanding
615  devp config set enable_cargo false       Put one back";
616
617pub const CONFIG_WIZARD_LONG: &str = "\
618Open every global setting in a full-screen configurator, with the `devp trust` \
619declaration in front of it: what this tool is allowed to do is on screen before any \
620of it is configurable.
621
622Enter is the \"keep going\" key: on a row with an untaken recommendation it takes \
623that advice and moves on, on any other row it just moves on, and on the Finish line \
624it opens a summary of exactly what will be written — one more Enter writes it. So \
625holding nothing but Enter reviews every setting, accepts the safe recommendations, \
626and finishes. The walk never takes the cautious tier (`allow_manifest_rewrite`); \
627turning that on stays a deliberate Space on its row. Arrows move without accepting \
628anything; Space changes the highlighted setting — a toggle flips, a number opens a \
629field, `disabled_adapters` opens the adapter checklist; `r` puts one back. `q` \
630leaves without saving anything, from anywhere.
631
632`devp config recommended` is the one-command version of the suggestions screen, for \
633when you know what you want and do not want to walk the list.
634
635It runs itself once on a first install — so the defaults are something you agreed \
636to, not something you inherited — and again after an upgrade adds a setting this \
637machine has never been shown, which it marks NEW and opens on. Settings you have \
638already confirmed are never re-asked.
639
640It never runs unattended: no TTY means skipped, not guessed at. `--no-tui`, and the \
641DEV_PRUNE_NO_TUI environment variable, ask one question per line instead — for \
642terminals the full-screen view cannot drive, and for agents, which hold a real \
643terminal and will never press a key. To configure this tool from a script, use \
644`devp config set <key> <value>`, which needs no terminal at all.";
645
646pub const CONFIG_WIZARD_EXAMPLES: &str = "\
647EXAMPLES:
648  devp config wizard
649  devp config wizard --no-tui              One question per line
650  devp config set disabled_adapters go     Leave Go projects alone entirely
651  devp config set disabled_adapters -      Every adapter active again";
652
653pub const RESTORE_LONG: &str = "\
654Put dependencies back: detect each project's lockfile and run its manager's install \
655(`npm ci`, `pnpm install`, `uv sync`, `cargo fetch`, …). Mirrors pruning — every \
656project in the tree is restored, each by its own manager, so a monorepo comes back \
657whole.
658
659`--last-run` restores exactly what the most recent prune pass deleted, across every \
660repository it touched, and nothing else: the undo for a `devp run`. Each prune \
661records what it removed (a dry run records nothing), and the flag fails cleanly if \
662no pass has been recorded yet. It cannot be combined with a path — silently ignoring \
663the path would restore the wrong thing.";
664
665pub const RESTORE_EXAMPLES: &str = "\
666EXAMPLES:
667  devp restore                    Restore the current directory's projects
668  devp restore ~/Code/my-app
669  devp restore --last-run         Undo the last prune pass, everywhere it acted";
670
671pub const UPDATE_LONG: &str = "\
672Print the installed version, ask GitHub for the latest release, and show the upgrade \
673command for how this copy was installed. When a newer release exists and stdin is a \
674terminal it then asks `Install vX.Y.Z now? [y/N]`; Enter leaves the binary alone, `y` \
675downloads the release, verifies its checksum and replaces every copy this install \
676runs, falling back to the package manager that owns this copy (cargo, npm, bun, pnpm, \
677yarn, uv, pipx, or the installer script) when there is no published binary for the \
678platform. `--install` or `-y` gives that answer up front; a copy run from a script or a \
679pipe is never asked and only prints the command. `--channels` prints the command for \
680every channel instead of only this one, and touches nothing. `auto_update` is on by default and does the verified-download half by \
681itself at the end of a prune pass when a newer release is known — never the \
682package-manager half, and nothing at all on WinGet, Scoop and Homebrew, where the \
683manager owns the upgrade; `devp config set auto_update false` stops it. An upgrade never interrupts the \
684scheduler: the scheduled pass runs a managed copy that refreshes itself from the \
685new binary on its next run.
686
687`devp config set version_lock true` outranks all of it. While the pin is on this \
688copy stays on the version it is: `auto_update` does not run however it is set, \
689`--install` refuses, `devp install --channel` refuses because moving channels \
690installs the latest release, and the install scripts leave the binary alone. \
691No flag bypasses it -- `devp config set version_lock false` is the way back.
692
693The same check also runs quietly from `devp run` and `devp status`, at most once \
694every `update_check_interval_days` (7 by default), printing one line only when a \
695newer version exists. It is the only thing in dev-prune that opens a network \
696connection, sends no body and no identifier, and `devp config set update_check \
697false` turns it off for good; `--offline` skips it for one run without changing the \
698setting.";
699
700pub const UPDATE_EXAMPLES: &str = "\
701EXAMPLES:
702  devp update                     Version, latest release, upgrade command, then [y/N]
703  devp update -y                  The same, answering the prompt with yes
704  devp update --install           Upgrade now, without the version report
705  devp update --channels          Every channel's upgrade command, no network
706  devp update --offline           No network this run";
707
708pub const INSTALL_LONG: &str = "\
709Move this installation from one package manager to another.
710
711`devp update` always upgrades the copy that is running, through whichever channel \
712installed it. This command changes *which* channel owns it: it installs through the \
713manager you name, then removes the old copy through the manager that put it there — in \
714that order, so a failed install leaves the working copy untouched.
715
716Removing the old copy through its own manager, rather than deleting the file, is the \
717point: uv, pipx, npm, cargo and the rest each keep a record of what they installed, and \
718a manager whose record still says dev-prune is there will put the old binary back.
719
720bun, pnpm and yarn install the same npm package and are each their own channel, not \
721npm. A copy `bun add -g dev-prune` put in place is upgraded with bun and removed with \
722bun; running npm against it installs a second copy under npm's prefix and leaves the \
723first one stale and still on PATH.
724
725Nothing is migrated, because nothing needs to be. Settings, the repository registry and \
726the undo history live in the config directory, which no package manager owns.
727
728With no `--channel` it prints which manager installed this copy and what `--channel` \
729accepts. `--dry-run` prints the commands without running any of them.";
730
731pub const INSTALL_EXAMPLES: &str = "\
732EXAMPLES:
733  devp install                              Which channel owns this copy
734  devp install --channel winget --dry-run   Print the plan, change nothing
735  devp install --channel uv                 Move onto uv, and remove the old copy
736  devp install --channel cargo --yes        Skip the confirmation prompt";
737
738pub const SKILL_LONG: &str = "\
739Teach your AI assistant this tool. Exports SKILL.md — the full agent-facing manual: \
740every command, the JSON contracts, the safety invariants, the troubleshooting tree — \
741into the config directory, installs it into any detected agent skills directory \
742(`~/.claude/skills/dev-prune/`, the same install `devp setup` performs), and prints \
743ready-to-copy onboarding prompts for assistants without one (Gemini Antigravity, \
744Cursor, Windsurf, Copilot, OpenClaw).
745
746`--agent <EDITOR>` instead writes per-repository rules into the current repository, in \
747the file that editor's agent reads. Ten editors get a file of their own — cursor, \
748windsurf, antigravity, cline, roo, kilocode, continue, amazon-q, kiro, trae — and six \
749share a file with other tools, so dev-prune owns a marked block inside it: agents-md \
750(`AGENTS.md`, the cross-tool convention Codex, Jules, Amp and OpenCode read), copilot \
751(`.github/copilot-instructions.md`), gemini (`GEMINI.md`), junie \
752(`.junie/guidelines.md`), zed (`.rules`) and aider (`CONVENTIONS.md`, the one file its \
753editor does not read by finding it — writing it prints the `read: CONVENTIONS.md` line \
754that makes Aider load it). Every byte outside the markers is left as found. \
755`devp skill --help` lists each value with its exact path. Claude Code needs no \
756per-repository file — its skill installs globally.";
757
758pub const SKILL_EXAMPLES: &str = "\
759EXAMPLES:
760  devp skill                      Export SKILL.md, print onboarding prompts
761  devp skill --agent cursor       Write .cursor/rules/dev-prune.mdc here
762  devp skill --agent agents-md    Upsert the marked block in AGENTS.md";
763
764pub const SETUP_LONG: &str = "\
765Install whatever integration is missing and leave the rest alone: the `devp` alias, \
766the managed binary directory on your PATH (a user PATH entry on Windows, \
767`~/.local/bin` symlinks elsewhere — what keeps `devp` working after the venv or npx \
768cache it came from disappears), the exported SKILL.md and its agent-directory \
769install, the file-manager icon registration, the global Git hooks, and the OS \
770scheduler. Safe to run repeatedly: it is the same pass the install scripts run, the \
771same one `devp init` runs, and the same one that runs by itself on the first command \
772after an upgrade. Typing it is also the durable yes to the first-run question: a \
773machine where the setup walkthrough was quit -- or never shown -- treats `devp setup` \
774as the answer, and upgrades maintain what it installed from then on.
775
776It skips rather than forces: Git hooks when `git` is missing or another tool holds \
777`core.hooksPath` (take the slot with `devp hook install --chain`), the alias when \
778the running process is `devp` itself on Windows, and anything switched off by \
779`auto_setup`, `auto_hooks`, `auto_daemon` or `DEV_PRUNE_NO_AUTO_SETUP=1`.
780
781When a VS Code-family editor is on your PATH (VS Code, VSCodium, Cursor, Windsurf, \
782Positron, Kiro, or an Insiders build) and the dev-prune extension is not installed, \
783one run also asks — once ever, only at a terminal — whether to install it into each \
784editor found. Each editor installs from its own registry; when a fork's registry does \
785not carry the extension, the `.vsix` from the extension's own newest release is \
786installed instead. Decline and it never asks again; install it yourself later with \
787`code --install-extension VKrishna04.dev-prune`.";
788
789pub const SETUP_EXAMPLES: &str = "\
790EXAMPLES:
791  devp setup                      Install what is missing, report what was skipped
792  devp setup --status             Report only; change nothing
793  DEV_PRUNE_NO_AUTO_SETUP=1 devp init ~/Code
794                                  Register repositories, install nothing";
795
796pub const DOCTOR_LONG: &str = "\
797One read-only pass that answers \"why is this not doing what I expect\". Without a \
798path it checks the installation: binary and twin, PATH, registry health, every \
799stored setting revalidated, SKILL.md, icons, hooks, scheduler, the package-manager \
800binaries your repositories actually need, and the release-check state. With a path \
801it checks that repository and ends by naming the single reason a prune would or \
802would not touch it.
803
804`--fix` is diagnosis first, then treatment — and it mends installed-but-broken only: \
805a stale or missing `devp` twin, a missing SKILL.md export, hooks or a scheduler \
806whose recorded binary moved, a drifted hook chain, and registry entries whose \
807repository is gone. Each repair is the corresponding `devp setup` pass re-run, so a \
808repair can never do more than setup itself would. It never performs a first-time \
809install, and never touches an unreadable registry — a parse failure is for you to \
810look at, not for a tool to guess at.
811
812EXIT CODES: 0 when everything works, warnings included — a missing scheduler should \
813not fail a script. 1 only for genuine breakage: an unreadable registry, an \
814out-of-range setting, a dead registered path, a directory that is not a Git \
815repository. `--fix` exits 1 when any repair failed or was out of reach.";
816
817pub const DOCTOR_EXAMPLES: &str = "\
818EXAMPLES:
819  devp doctor                     Check the installation
820  devp doctor .                   Why would a prune touch (or skip) this repository?
821  devp doctor ~/Code/api
822  devp doctor --fix               Repair what the installation check finds broken";
823
824pub const UNINSTALL_LONG: &str = "\
825Remove dev-prune from the machine: the OS scheduler, the global Git hooks (only if \
826`core.hooksPath` still points at dev-prune), the file-type icons, the installed \
827agent skill, the PATH entry (or `~/.local/bin` symlinks), and the binaries — the \
828managed pair, the copy you invoked, and, with your confirmation, every other copy \
829found on PATH or in the well-known install directories (`~/.cargo/bin`, \
830`~/.local/bin`, npm's global directory, pip's Scripts directories). A copy owned by \
831a package manager is listed with its manager, and after removal the manager's own \
832uninstall command is printed so its records can be cleared too.
833
834On Windows, where a running executable cannot delete itself, a detached helper \
835removes the last files a few seconds after the command exits — no reboot, no closed \
836terminal.
837
838Without `--deep` the configuration survives, so a reinstall picks up where you left \
839off. With `--deep` the global config folder and every registered repository's \
840`.devprune.json` go too; that asks for confirmation, and refuses outright with no \
841terminal to ask on unless `-y` is passed. Exits 1 if anything could not be removed, \
842naming each leftover.";
843
844pub const UNINSTALL_EXAMPLES: &str = "\
845EXAMPLES:
846  devp uninstall                  Remove the program; keep config for a reinstall
847  devp uninstall --deep           Also wipe config and per-repo .devprune.json (asks)
848  devp uninstall --deep -y        Non-interactive; also confirms the stray-copy sweep";
849
850// ---------------------------------------------------------------------------
851// The front page: how the commands are grouped, which of them stay off it, and
852// the colours the help is rendered in.
853// ---------------------------------------------------------------------------
854
855/// How the commands are grouped, and the one line each gets — on the manual's
856/// contents page (`devp man`) and in the categorised `devp --help` alike.
857///
858/// The lines are written here rather than taken from clap's `about`, which truncates
859/// into nonsense at this width ("Export SKILL", "View system dashboard"). Tests check
860/// this table against the real command list, so a command added without a line here
861/// fails the build rather than going missing from the pages a reader navigates from.
862pub const COMMAND_GROUPS: [(&str, &[(&str, &str)]); 5] = [
863    (
864        "Register repositories",
865        &[
866            (
867                "init",
868                "find every Git repository under a path, register them",
869            ),
870            ("link", "register one repository"),
871            ("unlink", "forget one — deletes nothing"),
872            ("undo", "revert the last init or link"),
873        ],
874    ),
875    (
876        "Prune and put back",
877        &[
878            ("run", "delete what a lockfile proves comes back"),
879            ("restore", "reinstall what was deleted"),
880        ],
881    ),
882    (
883        "Look at what is going on",
884        &[
885            ("status", "every repository, its size and its idle days"),
886            ("stats", "space reclaimed over time"),
887            ("history", "which pass deleted what, and what asked it to"),
888            ("caches", "package manager caches on this machine"),
889            ("doctor", "what is broken, and how to fix it"),
890            ("trust", "what this program may do on this machine"),
891        ],
892    ),
893    (
894        "Settings and integration",
895        &[
896            ("config", "settings, the scheduler, Git hooks, icons"),
897            ("setup", "install whatever integration is missing"),
898            ("skill", "rules files for your editor's AI agent"),
899            ("completions", "a completion script for your shell"),
900            ("man", "this manual"),
901        ],
902    ),
903    (
904        "The program itself",
905        &[
906            ("update", "check for a newer release, and install it"),
907            ("install", "move it to another package manager"),
908            ("uninstall", "remove it, integration included"),
909        ],
910    ),
911];
912
913/// Commands that still run but no longer appear in `devp --help`.
914///
915/// `undo` covers ground `restore` also covers, and `install` *moves* an installation
916/// rather than performing one — but both shipped in 1.0.0 and the CLI surface is a
917/// contract, so the rename and the removal wait for 2.x. Until then they leave the
918/// front view only: still invocable, still answering their own `--help`, still in
919/// `devp man` and the reference. The manual keeps listing them, because a manual is
920/// where a thing is looked up, not where it is discovered.
921pub const HIDDEN_FROM_HELP: &[&str] = &["undo", "install"];
922
923/// The palette the help is rendered in. Set once on the top-level command and
924/// propagated by clap to every subcommand, so `devp run --help` matches the front
925/// page. clap drops the colour when stdout is not a terminal, the same way `colored`
926/// does for the hand-built block below.
927pub const HELP_STYLES: clap::builder::Styles = clap::builder::Styles::styled()
928    .header(clap::builder::styling::AnsiColor::Green.on_default().bold())
929    .usage(clap::builder::styling::AnsiColor::Green.on_default().bold())
930    .literal(clap::builder::styling::AnsiColor::Cyan.on_default().bold())
931    .placeholder(clap::builder::styling::AnsiColor::Cyan.on_default());
932
933/// The commands section of `devp --help`: [`COMMAND_GROUPS`] minus
934/// [`HIDDEN_FROM_HELP`], group titles and command names coloured.
935///
936/// The padding is applied to the plain name before it is coloured — `{:<12}` on the
937/// coloured string would count the escape codes as width and misalign every line
938/// exactly when the colours are on.
939fn grouped_commands() -> String {
940    use colored::Colorize;
941    let mut out = String::new();
942    for (title, entries) in COMMAND_GROUPS {
943        let visible: Vec<&(&str, &str)> = entries
944            .iter()
945            .filter(|(name, _)| !HIDDEN_FROM_HELP.contains(name))
946            .collect();
947        if visible.is_empty() {
948            continue;
949        }
950        out.push_str(&format!("\n  {}\n", title.bold()));
951        for (name, line) in visible {
952            let pad = " ".repeat(12usize.saturating_sub(name.len()));
953            out.push_str(&format!("    {}{pad}  {line}\n", name.cyan().bold()));
954        }
955    }
956    out
957}
958
959/// The top-level help template: clap's default, with the flat twenty-command list
960/// swapped for the grouped block above. Only the top level gets it — a subcommand's
961/// own list is short enough not to need grouping. The section headings are literal
962/// text here, coloured to match [`HELP_STYLES`], because clap's `{options}` tag
963/// renders the entries without one.
964pub static ROOT_HELP_TEMPLATE: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
965    use colored::Colorize;
966    format!(
967        "{{before-help}}{{about-with-newline}}\n{{usage-heading}} {{usage}}\n\n{commands}\n{block}\n{options}\n{{options}}{{after-help}}",
968        commands = "Commands:".green().bold(),
969        block = grouped_commands(),
970        options = "Options:".green().bold(),
971    )
972});
973
974/// The examples block under the help. Built at runtime for the same reason the
975/// template is: the headings carry colour.
976pub static ROOT_AFTER_HELP: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
977    use colored::Colorize;
978    format!(
979        "{examples}
980  devp init ~/Code          Scan directory trees & onboard workspaces
981  devp link                 Register current repository
982  devp run                  Execute prune pass across inactive repositories
983  devp status               View system status dashboard
984  devp status --top 10      Show only the ten biggest reclaims
985  devp stats                Lifetime totals, recent passes, biggest repositories
986  devp caches               Size every package manager cache (deletes nothing)
987  devp completions powershell   Emit a shell completion script
988  devp status daemon        Check background daemon status (alias for `devp config daemon status`)
989  devp status . hook        Check workspace Git hook status (alias for `devp config . hook status`)
990  devp config . daemon disable  Disable daemon background pass for current workspace
991  devp restore .            Restore missing node_modules/.venv via lockfile
992
993{alias}
994  `dev-prune` and `devp` invoke the exact same executable.
995
996dev-prune is written by VKrishna04 and licensed Apache-2.0.
997  https://github.com/Life-Experimentalist/dev-prune",
998        examples = "EXAMPLES:".green().bold(),
999        alias = "BINARY ALIAS:".green().bold(),
1000    )
1001});
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006    use clap::CommandFactory;
1007
1008    #[test]
1009    fn hidden_from_help_is_exactly_what_the_cli_hides() {
1010        // The front page is built from HIDDEN_FROM_HELP, not from clap, so the two
1011        // can only stay in step if a test holds them together.
1012        let mut command = crate::Cli::command();
1013        command.build();
1014        for sub in command.get_subcommands() {
1015            if sub.get_name() == "help" {
1016                continue;
1017            }
1018            assert_eq!(
1019                sub.is_hide_set(),
1020                HIDDEN_FROM_HELP.contains(&sub.get_name()),
1021                "`{}` disagrees with HIDDEN_FROM_HELP",
1022                sub.get_name()
1023            );
1024        }
1025    }
1026
1027    #[test]
1028    fn the_front_page_lists_every_visible_command_and_no_hidden_one() {
1029        // Colour off so the assertion sees the names, not the escape codes around
1030        // them. The harness pipe would disable it anyway; this makes it not matter.
1031        colored::control::set_override(false);
1032        let block = grouped_commands();
1033        colored::control::unset_override();
1034
1035        let mut command = crate::Cli::command();
1036        command.build();
1037        for sub in command.get_subcommands() {
1038            let name = sub.get_name();
1039            if name == "help" {
1040                continue;
1041            }
1042            // Anchored to the start of a line: `install` appears mid-line in
1043            // setup's description, and inside `uninstall`, without being listed.
1044            let probe = format!("    {name} ");
1045            let listed = block.lines().any(|l| l.starts_with(&probe));
1046            assert_eq!(!sub.is_hide_set(), listed, "`{name}`");
1047        }
1048    }
1049}