Skip to main content

oxibrain_cli/cmd/
spaces.rs

1//! `oxibrain spaces` — list every space with live counts.
2//!
3//! Read-only: opens the store with `Brain::open_ro`, so it takes no
4//! advisory lock and coexists with a running daemon (§16.1).
5
6use anyhow::Result;
7use oxibrain::{Brain, BrainConfig};
8use std::path::Path;
9
10pub async fn run(dir: &Path) -> Result<()> {
11    let brain = Brain::open_ro(BrainConfig::at(dir)).await?;
12    let spaces = brain.list_spaces().await?;
13    println!(
14        "{:<24} {:<16} {:<20} {:>9} {:>9}",
15        "NAME", "ID", "CREATED", "EPISODES", "ENTITIES"
16    );
17    for s in &spaces {
18        let id = s.id.chars().take(16).collect::<String>();
19        println!(
20            "{:<24} {:<16} {:<20} {:>9} {:>9}",
21            s.name,
22            id,
23            millis_to_iso(s.created_at.millis()),
24            s.episode_count,
25            s.entity_count
26        );
27    }
28    Ok(())
29}
30
31/// Minimal UTC formatting without a chrono dependency (store-independent).
32fn millis_to_iso(ms: i64) -> String {
33    // days-since-epoch civil conversion (Howard Hinnant's algorithm)
34    let secs = ms.div_euclid(1000);
35    let days = secs.div_euclid(86_400);
36    let rem = secs.rem_euclid(86_400);
37    let (h, m, s) = (rem / 3600, (rem % 3600) / 60, rem % 60);
38    let z = days + 719_468;
39    let era = z.div_euclid(146_097);
40    let doe = z.rem_euclid(146_097);
41    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
42    let y = yoe + era * 400;
43    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
44    let mp = (5 * doy + 2) / 153;
45    let d = doy - (153 * mp + 2) / 5 + 1;
46    let month = if mp < 10 { mp + 3 } else { mp - 9 };
47    let year = if month <= 2 { y + 1 } else { y };
48    format!("{year:04}-{month:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[tokio::test]
56    async fn spaces_prints_table() {
57        // Seed an initialized store so `Brain::open_ro` succeeds
58        // (`read_only_open_fails_on_missing_store` in facade.rs requires
59        // an initialized directory).
60        let dir = tempfile::TempDir::new().unwrap();
61        let brain = Brain::open(BrainConfig::at(dir.path())).await.unwrap();
62        let _ = brain.ensure_space("work").await.unwrap();
63        drop(brain);
64
65        // Run the read-only listing; asserts no error.
66        run(dir.path()).await.unwrap();
67    }
68
69    #[test]
70    fn millis_to_iso_epoch_zero() {
71        assert_eq!(millis_to_iso(0), "1970-01-01T00:00:00Z");
72    }
73
74    #[test]
75    fn millis_to_iso_known_timestamp() {
76        // 2024-01-15T12:34:56Z == 1_705_322_096_000 ms
77        assert_eq!(millis_to_iso(1_705_322_096_000), "2024-01-15T12:34:56Z");
78    }
79}