1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//! Command-line argument definitions for the `cargo-target-gc` binary.
use std::path::PathBuf;
use clap::{Parser, Subcommand};
/// cargo-target-gc — Cargo target-artifact garbage collection without throwing away hot build cache.
#[derive(Debug, Parser)]
#[command(
name = "cargo-target-gc",
version,
about,
long_about = None,
after_help = "Run `cargo target-gc` from the same directory where you would run `cargo build`.\nFor projects built through wrappers such as `make`, cd into the Cargo project\nor workspace directory used by that wrapper and run it there."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Analyze target directories and estimate space that can be reclaimed without deleting hot artifacts.
#[command(
after_help = "Run this from the Cargo project/workspace root: the same directory where `cargo build` would create target/. cargo-target-gc does not search opaque wrapper build paths."
)]
Scan {
/// Cargo project or workspace path to inspect (defaults to the current directory).
#[arg(long)]
path: Option<PathBuf>,
/// Emit raw-byte JSON instead of the human-readable storage report.
#[arg(long)]
json: bool,
},
/// Remove reclaimable target artifacts after an explicit dry-run or confirmation.
#[command(
after_help = "Run this from the Cargo project/workspace root: the same directory where `cargo build` would create target/. Use --dry-run first when checking an unfamiliar project."
)]
Clean {
/// Cargo project or workspace path to clean (defaults to the current directory).
#[arg(long)]
path: Option<PathBuf>,
/// Emit raw-byte JSON instead of the human-readable cleanup report.
#[arg(long)]
json: bool,
/// Preview removals without deleting anything; cannot be combined with --confirm.
#[arg(long, conflicts_with = "confirm")]
dry_run: bool,
/// Execute removals for reclaimable artifacts inside validated target roots.
#[arg(long)]
confirm: bool,
/// Allow cleaning even when an active Cargo/rustc process is detected.
#[arg(long)]
force_active: bool,
/// Refuse confirmed cleanup above this size unless increased (for example: 500MiB, 10GiB).
#[arg(long, value_parser = parse_size)]
max_reclaim: Option<u64>,
/// Also reclaim stale profile artifacts, not just incremental caches.
#[arg(long)]
stale: bool,
/// Also reclaim fresh incremental cache and Cargo profile cache directories.
#[arg(long)]
profile_cache: bool,
},
/// Show the effective retention and crate-scope configuration.
Config {
/// Cargo project path whose target-gc.toml should be resolved (defaults to cwd).
#[arg(long)]
path: Option<PathBuf>,
},
/// Install host-agent skills that teach agents how to safely run cargo-target-gc.
InstallAgentSkills {
/// Claude Code skills directory (defaults to ~/.claude/skills).
#[arg(long)]
claude_skills_dir: Option<PathBuf>,
/// Codex skills directory (defaults to ~/.codex/skills).
#[arg(long)]
codex_skills_dir: Option<PathBuf>,
/// Install for all supported hosts instead of only detected hosts.
#[arg(long, conflicts_with = "only")]
all: bool,
/// Install only for selected hosts, comma-separated: claude,codex.
#[arg(long, value_name = "HOSTS")]
only: Option<String>,
/// Approve all detected host installs without prompting.
#[arg(long)]
yes: bool,
/// Print planned installs without writing files.
#[arg(long)]
dry_run: bool,
/// Overwrite existing cargo-target-gc skills without prompting.
#[arg(long, conflicts_with = "skip_existing")]
force: bool,
/// Keep existing cargo-target-gc skills without prompting.
#[arg(long)]
skip_existing: bool,
},
}
/// Parse a byte size with optional binary suffix: B, KiB, MiB, GiB, TiB.
fn parse_size(value: &str) -> Result<u64, String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err("size cannot be empty".into());
}
let split_at = trimmed
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(trimmed.len());
let (digits, suffix) = trimmed.split_at(split_at);
if digits.is_empty() {
return Err(format!("size {value:?} must start with a number"));
}
let base: u64 = digits
.parse()
.map_err(|_| format!("size {value:?} is not a valid integer"))?;
let multiplier = match suffix.trim().to_ascii_lowercase().as_str() {
"" | "b" => 1,
"kib" | "k" | "kb" => 1024,
"mib" | "m" | "mb" => 1024_u64.pow(2),
"gib" | "g" | "gb" => 1024_u64.pow(3),
"tib" | "t" | "tb" => 1024_u64.pow(4),
other => return Err(format!("unsupported size suffix {other:?}")),
};
base.checked_mul(multiplier)
.ok_or_else(|| format!("size {value:?} is too large"))
}