Skip to main content

warden/commands/
mod.rs

1//! Command implementations: the thin layer between the CLI and the library.
2//!
3//! Every command builds a [`crate::output::Report`] and hands it to
4//! [`crate::output::emit`]; `emit` is the only place that branches on `--json`.
5//! `ingest`, `doctor`, and `watch` render prose via
6//! [`crate::output::Report::prose`] rather than a table; either shape still goes
7//! out through `emit`.
8
9pub mod doctor;
10pub mod ingest;
11pub mod purge;
12pub mod query;
13pub mod report;
14pub mod suggest;
15pub mod watch;
16
17use std::io;
18
19use crate::cli::TimeWindow;
20use crate::config::Config;
21use crate::reports::ReportCtx;
22use crate::store::StorePaths;
23
24/// The resolved global flags a reporting command needs.
25///
26/// Borrowed rather than owned so `main` can resolve config and paths once and
27/// hand the same view to every command.
28#[derive(Debug, Clone, Copy)]
29pub struct Env<'a> {
30    pub config: &'a Config,
31    pub paths: &'a StorePaths,
32    pub window: TimeWindow,
33    pub project: Option<&'a str>,
34    pub json: bool,
35    pub no_ingest: bool,
36    /// Sidechain (subagent) events are real spend, so they are in by default.
37    pub include_sidechain: bool,
38}
39
40impl Env<'_> {
41    /// The reporting context, priced from the *current* config: cost is derived
42    /// at read time, so an edit to `[pricing.*]` re-prices the store on the next
43    /// report rather than only on newly ingested events.
44    pub fn ctx(&self) -> ReportCtx {
45        ReportCtx::new(
46            self.window,
47            self.project.map(str::to_string),
48            self.include_sidechain,
49        )
50        .with_pricing(self.config.pricing())
51    }
52
53    /// The implicit ingest before a report, unless `--no-ingest`.
54    ///
55    /// Its progress goes to **stderr**: stdout belongs to the report, and in
56    /// `--json` mode it must stay a single parseable document.
57    pub fn pre_ingest(&self) -> io::Result<()> {
58        if self.no_ingest {
59            return Ok(());
60        }
61        let report = ingest::run_quiet(self.config, self.paths, self.window, self.project)?;
62        ingest::write_lines(&mut io::stderr(), &report)
63    }
64}
65
66/// `1203` → `1,203`. Counts in this output are read by humans.
67pub(crate) fn thousands(n: u64) -> String {
68    let digits = n.to_string();
69    let mut out = String::with_capacity(digits.len() + digits.len() / 3);
70    for (i, ch) in digits.chars().enumerate() {
71        if i > 0 && (digits.len() - i).is_multiple_of(3) {
72            out.push(',');
73        }
74        out.push(ch);
75    }
76    out
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn groups_digits_in_threes() {
85        assert_eq!(thousands(0), "0");
86        assert_eq!(thousands(999), "999");
87        assert_eq!(thousands(1_203), "1,203");
88        assert_eq!(thousands(1_000_000), "1,000,000");
89    }
90}