atagen

roundabout

public
Star0
Watch0
56a853ebf2549fd63ad8a481423b088d23dd0dc1
1
ssh://brewery@pls.lobotomise.me:33/atagen/roundabout.git
src/cli.rsRust326 lines8.7 KB
1
use std::{io, os::unix::net::UnixStream, path::PathBuf};
2
3
use pound::Parse;
4
5
use crate::{
6
    artifacts, client,
7
    config::Config,
8
    daemon, history, hook, paths,
9
    proto::{self, Event, Reader, Request},
10
    run,
11
    units::{Amount, Bytes, Dur},
12
};
13
14
/// one build at a time, please
15
#[derive(Parse)]
16
#[pound(name = "rabt", version = env!("CARGO_PKG_VERSION"))]
17
pub enum Rabt {
18
    /// wait your turn, then run the thing
19
    Run {
20
        /// preset from config, s m l xl out of the box
21
        #[pound(short, long)]
22
        size: Option<String>,
23
        #[pound(long)]
24
        ram: Option<Bytes>,
25
        #[pound(long)]
26
        disk: Option<Bytes>,
27
        /// count or percentage of the machine
28
        #[pound(long)]
29
        cores: Option<Amount<u32>>,
30
        /// how long you reckon it'll take
31
        #[pound(short, long)]
32
        time: Option<Dur>,
33
        /// kill it past this, exit 124
34
        #[pound(long)]
35
        deadline: Option<Dur>,
36
        /// stop queueing past this, exit 75
37
        #[pound(long)]
38
        max_wait: Option<Dur>,
39
        #[pound(long, hidden, env = "CLAUDE_CODE_SESSION_ID")]
40
        session: Option<String>,
41
        #[pound(trailing)]
42
        cmd: Vec<String>,
43
    },
44
    /// who's running, who's waiting
45
    Status {
46
        #[pound(long)]
47
        json: bool,
48
    },
49
    /// a size guess from past runs
50
    Suggest {
51
        #[pound(trailing)]
52
        cmd: Vec<String>,
53
    },
54
    /// stop a lease's build
55
    Kill { lease: u64 },
56
    /// buy an overdue build more time
57
    Extend { lease: u64, by: Dur },
58
    /// exit when a lease goes overdue or finishes
59
    Watch { lease: u64 },
60
    /// build output and how much of it there is
61
    Artifacts {
62
        #[pound(subcommand)]
63
        action: Artifacts,
64
    },
65
    /// claude code hook entrypoint, json on stdin
66
    Hook {
67
        /// pre-tool-use post-tool-use prompt-submit stop subagent-stop
68
        event: String,
69
    },
70
    #[pound(hidden)]
71
    Daemon,
72
}
73
74
#[derive(Parse)]
75
pub enum Artifacts {
76
    /// sizes against the caps, exits 1 when over
77
    Check {
78
        /// re-measure instead of trusting the cache
79
        #[pound(long)]
80
        fresh: bool,
81
    },
82
    /// everything tracked, biggest first
83
    List,
84
    /// delete idle artifacts
85
    Gc {
86
        /// keep going, oldest first, until under the caps
87
        #[pound(long)]
88
        cap: bool,
89
        #[pound(short = 'n', long)]
90
        dry_run: bool,
91
    },
92
    /// never gc this
93
    Pin {
94
        path: PathBuf,
95
    },
96
    Unpin {
97
        path: PathBuf,
98
    },
99
}
100
101
fn artifacts(cfg: &Config, action: Artifacts) -> i32 {
102
    let cwd = std::env::current_dir().unwrap_or_else(|_| "/".into());
103
    let root = paths::project_root(&cwd);
104
    match action {
105
        Artifacts::Check { fresh } => {
106
            let r = artifacts::check(cfg, Some(&root), fresh);
107
            if let Some((root, size)) = &r.project {
108
                println!(
109
                    "{}: {size} of {}",
110
                    root.display(),
111
                    cfg.artifacts.project_cap
112
                );
113
            }
114
            println!("everything: {} of {}", r.global, cfg.artifacts.global_cap);
115
            if r.over() {
116
                println!("over. `rabt artifacts gc --cap`");
117
                1
118
            } else {
119
                0
120
            }
121
        }
122
        Artifacts::List => {
123
            let r = artifacts::check(cfg, Some(&root), false);
124
            let mut rows: Vec<_> = r.entries.iter().collect();
125
            rows.sort_by_key(|(_, e)| std::cmp::Reverse(e.size));
126
            for (path, e) in rows {
127
                println!(
128
                    "{:>10}  {}{}",
129
                    e.size.to_string(),
130
                    path.display(),
131
                    if e.pinned { "  (pinned)" } else { "" }
132
                );
133
            }
134
            0
135
        }
136
        Artifacts::Gc { cap, dry_run } => {
137
            let g = artifacts::gc(cfg, cap, dry_run);
138
            for p in &g.removed {
139
                println!(
140
                    "{}{}",
141
                    if dry_run { "would remove " } else { "removed " },
142
                    p.display()
143
                );
144
            }
145
            for f in &g.failed {
146
                eprintln!("rabt: {f}");
147
            }
148
            println!(
149
                "{} {}",
150
                if dry_run { "would free" } else { "freed" },
151
                g.freed
152
            );
153
            i32::from(!g.failed.is_empty())
154
        }
155
        Artifacts::Pin { path } | Artifacts::Unpin { path } if !path.exists() => {
156
            eprintln!("rabt: {} doesn't exist", path.display());
157
            1
158
        }
159
        Artifacts::Pin { path } => report(artifacts::pin(&path, true)),
160
        Artifacts::Unpin { path } => report(artifacts::pin(&path, false)),
161
    }
162
}
163
164
fn report(r: Result<(), String>) -> i32 {
165
    r.map_or_else(
166
        |e| {
167
            eprintln!("rabt: {e}");
168
            1
169
        },
170
        |()| 0,
171
    )
172
}
173
174
fn ask(req: &Request) -> io::Result<Option<Event>> {
175
    let Some(mut s) = client::connect() else {
176
        return Ok(None);
177
    };
178
    proto::send(&mut s, req)?;
179
    Reader::new(&s)?.recv()
180
}
181
182
fn reply(ev: io::Result<Option<Event>>) -> i32 {
183
    match ev {
184
        Ok(Some(Event::Ok)) => 0,
185
        Ok(Some(Event::Err { msg })) => {
186
            eprintln!("rabt: {msg}");
187
            1
188
        }
189
        Ok(None) => {
190
            eprintln!("rabt: nothing running");
191
            1
192
        }
193
        Ok(Some(_)) => 1,
194
        Err(e) => {
195
            eprintln!("rabt: {e}");
196
            1
197
        }
198
    }
199
}
200
201
fn status(json: bool) -> i32 {
202
    let Ok(Some(Event::Status(st))) = ask(&Request::Status) else {
203
        if json {
204
            println!("null");
205
        } else {
206
            println!("idle");
207
        }
208
        return 0;
209
    };
210
    if json {
211
        println!("{}", serde_json::to_string(&st).unwrap_or_default());
212
        return 0;
213
    }
214
    println!(
215
        "ram {} free of {}, cores {} free of {}",
216
        st.ram_free, st.ram_budget, st.cores_free, st.cores
217
    );
218
    for l in &st.leases {
219
        let state = if l.running {
220
            format!(
221
                "running {}{}",
222
                Dur::secs(l.elapsed),
223
                if l.overdue { " OVERDUE" } else { "" }
224
            )
225
        } else {
226
            format!(
227
                "#{} {}",
228
                l.position,
229
                l.waiting.map(|r| r.to_string()).unwrap_or_default()
230
            )
231
        };
232
        println!(
233
            "{:>4} {}{} {} ram {} cores {} rss {}  {}",
234
            l.id,
235
            if l.heavy { "H " } else { "" },
236
            state,
237
            Dur(l.need.time),
238
            l.need.ram,
239
            l.need.cores,
240
            l.rss,
241
            l.cmd.join(" ")
242
        );
243
    }
244
    0
245
}
246
247
fn suggest(cfg: &Config, cmd: &[String]) -> i32 {
248
    let cwd = std::env::current_dir().unwrap_or_else(|_| "/".into());
249
    let key = history::key(&paths::project_root(&cwd), cmd);
250
    let Some(rec) = history::lookup(&key) else {
251
        println!("never seen it, pick a --size");
252
        return 1;
253
    };
254
    let base = match run::base(cfg) {
255
        Ok(b) => b,
256
        Err(e) => {
257
            eprintln!("rabt: {e}");
258
            return 2;
259
        }
260
    };
261
    let n = history::suggest(&rec, base, cmd);
262
    println!(
263
        "--ram {} --cores {} --time {} --disk {}",
264
        n.ram,
265
        n.cores,
266
        Dur(n.time),
267
        n.disk
268
    );
269
    0
270
}
271
272
fn watch(lease: u64) -> i32 {
273
    let Some(mut s): Option<UnixStream> = client::connect() else {
274
        return 0;
275
    };
276
    if proto::send(&mut s, &Request::Watch { lease }).is_err() {
277
        return 0;
278
    }
279
    if matches!(
280
        Reader::new(&s).and_then(|mut r| r.recv::<Event>()),
281
        Ok(Some(Event::Fired { overdue: true }))
282
    ) {
283
        println!("lease {lease} is overdue, `rabt kill {lease}` or `rabt extend {lease} <dur>`");
284
        3
285
    } else {
286
        println!("lease {lease} is done");
287
        0
288
    }
289
}
290
291
pub fn dispatch(cfg: &Config, cmd: Rabt) -> i32 {
292
    match cmd {
293
        Rabt::Run {
294
            size,
295
            ram,
296
            disk,
297
            cores,
298
            time,
299
            deadline,
300
            max_wait,
301
            session,
302
            cmd,
303
        } => run::run(
304
            cfg,
305
            &run::Args {
306
                size,
307
                ram,
308
                disk,
309
                cores,
310
                time,
311
                deadline,
312
                max_wait,
313
                session,
314
                cmd,
315
            },
316
        ),
317
        Rabt::Status { json } => status(json),
318
        Rabt::Suggest { cmd } => suggest(cfg, &cmd),
319
        Rabt::Kill { lease } => reply(ask(&Request::Kill { lease })),
320
        Rabt::Extend { lease, by } => reply(ask(&Request::Extend { lease, by })),
321
        Rabt::Watch { lease } => watch(lease),
322
        Rabt::Artifacts { action } => artifacts(cfg, action),
323
        Rabt::Hook { event } => hook::main(cfg, &event),
324
        Rabt::Daemon => daemon::main(Config::load().unwrap_or_default()),
325
    }
326
}