Skip to main content

grex_cli/cli/verbs/
status.rs

1//! `grex status` — drift vs lockfile.
2//!
3//! Walks the pack tree and runs the sync pipeline with `dry_run = true`.
4//! Per-pack drift is derived from the resulting [`SyncReport`]: packs
5//! with zero would-execute steps are reported as `clean`; packs with N
6//! steps are reported as `would-update N`. The verb never mutates state
7//! — the dry-run guarantee (B4) is preserved at the `SyncOptions` layer.
8
9use crate::cli::args::{GlobalFlags, StatusArgs};
10use anyhow::Result;
11use grex_core::sync::{self, SyncOptions, SyncReport};
12use std::collections::BTreeMap;
13use tokio_util::sync::CancellationToken;
14
15pub fn run(args: StatusArgs, global: &GlobalFlags, cancel: &CancellationToken) -> Result<()> {
16    let Some(pack_root) = super::resolve_pack_root_or_cwd(args.pack_root.as_deref()) else {
17        emit_error(
18            global.json,
19            "usage",
20            "`<pack_root>` required (directory with `.grex/pack.yaml`)",
21        );
22        std::process::exit(2);
23    };
24    let opts = SyncOptions::new().with_dry_run(true).with_validate(true);
25    match sync::run(&pack_root, &opts, cancel) {
26        Ok(report) => {
27            render(&report, global.json);
28            if report.halted.is_some() {
29                std::process::exit(2);
30            }
31            Ok(())
32        }
33        Err(err) => {
34            let outcome = super::sync::classify_sync_err(err, global.json, "status");
35            match outcome {
36                super::sync::RunOutcome::Validation => std::process::exit(1),
37                super::sync::RunOutcome::Tree | super::sync::RunOutcome::UsageError => {
38                    std::process::exit(2)
39                }
40                super::sync::RunOutcome::Exec => std::process::exit(2),
41                super::sync::RunOutcome::Ok => Ok(()),
42            }
43        }
44    }
45}
46
47fn render(report: &SyncReport, json: bool) {
48    let mut per_pack: BTreeMap<String, usize> = BTreeMap::new();
49    for s in &report.steps {
50        *per_pack.entry(s.pack.clone()).or_insert(0) += 1;
51    }
52    if json {
53        let packs: Vec<serde_json::Value> = per_pack
54            .iter()
55            .map(|(pack, &count)| {
56                let state = if count == 0 { "clean" } else { "would_update" };
57                serde_json::json!({
58                    "path": pack,
59                    "state": state,
60                    "drift_count": count,
61                })
62            })
63            .collect();
64        let clean = per_pack.values().all(|&n| n == 0);
65        let doc = serde_json::json!({
66            "verb": "status",
67            "clean": clean,
68            "packs": packs,
69        });
70        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
71    } else if per_pack.is_empty() {
72        println!("clean");
73    } else {
74        for (pack, count) in &per_pack {
75            if *count == 0 {
76                println!("clean    {pack}");
77            } else {
78                println!("would-update {count:>3} {pack}");
79            }
80        }
81    }
82}
83
84fn emit_error(json: bool, kind: &str, msg: &str) {
85    if json {
86        let doc = serde_json::json!({
87            "verb": "status",
88            "error": { "kind": kind, "message": msg },
89        });
90        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
91    } else {
92        eprintln!("grex status: {msg}");
93    }
94}