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