1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//! `mecha work` — what runs have generated, and removing what is past.
//!
//! The human half of [`mecha_core::work`]. A producer's directory is the
//! workspace its runs are jailed to, so this is also the answer to "where did
//! the briefing go" and "what can that trigger actually see".
//!
//! `clean` is the retention policy made real. It is deliberately loud about
//! what it removed — a sweep that prints nothing is one nobody trusts enough
//! to schedule, and the nightly runs it unattended.
use anyhow::{Context, Result};
use mecha_core::work;
#[derive(clap::Args, Debug)]
pub struct Args {
#[command(subcommand)]
pub cmd: Option<Cmd>,
}
#[derive(clap::Subcommand, Debug)]
pub enum Cmd {
/// What each producer has generated (default).
List,
/// Print one producer's directory, creating it if absent — for `cd $(...)`
/// and for a shell script that wants somewhere to write.
Path { producer: String },
/// Remove all but the newest N entries per producer.
Clean {
/// How many entries survive per producer. Defaults to `[work] keep`.
#[arg(long)]
keep: Option<usize>,
/// Only this producer.
#[arg(long)]
producer: Option<String>,
/// Say what would go, and remove nothing.
#[arg(long)]
dry_run: bool,
},
}
pub async fn execute(args: Args) -> Result<()> {
match args.cmd.unwrap_or(Cmd::List) {
Cmd::List => list(),
Cmd::Path { producer } => {
println!("{}", work::ensure(&producer)?.display());
Ok(())
}
Cmd::Clean {
keep,
producer,
dry_run,
} => clean(keep, producer.as_deref(), dry_run),
}
}
fn list() -> Result<()> {
let producers = work::list()?;
if producers.is_empty() {
println!(
"no work directories yet — {} is where a run's generated output goes",
work::root()?.display()
);
return Ok(());
}
for p in &producers {
println!(
"{:<24} {:>4} entr{} {:>8} {}",
p.name,
p.entries.len(),
if p.entries.len() == 1 { "y" } else { "ies" },
human(p.bytes),
p.path.display()
);
// The newest entry is the one a later run reads back, so name it.
if let Some(newest) = p.entries.first() {
println!(
" newest {}",
newest
.path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default()
);
}
}
Ok(())
}
fn clean(keep: Option<usize>, producer: Option<&str>, dry_run: bool) -> Result<()> {
let keep = match keep {
Some(k) => k,
None => {
let cwd = std::env::current_dir().context("cannot determine the working directory")?;
mecha_core::config::Config::load(&cwd)?.work.keep
}
};
let report = work::clean(keep, producer, dry_run)?;
if report.removed.is_empty() && report.protected.is_empty() {
println!("nothing to remove: every producer is within the last {keep}");
return Ok(());
}
for entry in &report.removed {
println!(
"{} {}",
if dry_run { "would remove" } else { "removed" },
entry.path.display()
);
}
// Named rather than silent: an entry that survives its own retention
// window reads as a bug in the sweep unless the reason is on screen.
for entry in &report.protected {
println!(
"kept (a published bundle names it as a source) {}",
entry.path.display()
);
}
println!(
"{} {} entr{}, {}",
if dry_run { "would free" } else { "freed" },
report.removed.len(),
if report.removed.len() == 1 {
"y"
} else {
"ies"
},
human(report.bytes_removed())
);
Ok(())
}
fn human(bytes: u64) -> String {
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
let mut value = bytes as f64;
let mut unit = 0;
while value >= 1024.0 && unit < UNITS.len() - 1 {
value /= 1024.0;
unit += 1;
}
if unit == 0 {
format!("{bytes} B")
} else {
format!("{value:.1} {}", UNITS[unit])
}
}