Skip to main content

memstead_cli/commands/
due.rs

1//! `memstead due` — render the due-brief: every open entity whose
2//! schema-declared due date falls inside the window, overdue first,
3//! across every mem whose schema declares the axis (read-only mounts
4//! labelled as third-party quoted data). The renderer is the shared
5//! engine entry point `Engine::render_due_brief`, so the CLI and
6//! UniFFI serve byte-identical content — the projection-brief
7//! precedent. There is deliberately no MCP tool (briefs are the
8//! CLI/app family); the MCP server instructions name this verb as the
9//! CLI companion.
10
11use clap::Args as ClapArgs;
12use serde_json::json;
13
14use crate::CliError;
15use crate::output::{ExitKind, print_json, print_markdown};
16use crate::setup::CliContext;
17
18#[derive(ClapArgs, Debug)]
19pub struct Args {
20    /// Relative window against today: `<N>d` days, `<N>m` calendar
21    /// months, `<N>y` calendar years (e.g. 90d, 6m, 2y). Everything
22    /// already overdue is always included. Default: 90d.
23    #[arg(long, default_value = memstead_base::engine::due::DEFAULT_DUE_WINDOW)]
24    pub within: String,
25
26    /// Restrict the brief to one mem (default: every mounted mem whose
27    /// schema declares a due axis).
28    #[arg(long)]
29    pub mem: Option<String>,
30
31    /// Override the current date (YYYY-MM-DD). Testing hook — the
32    /// brief is deterministic given the store and this date.
33    #[arg(long, hide = true)]
34    pub today: Option<String>,
35}
36
37pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
38    let window = memstead_base::engine::due::parse_due_window(&args.within)
39        .map_err(|msg| CliError::new(ExitKind::Validation, "INVALID_INPUT", msg))?;
40    let today = match &args.today {
41        Some(t) => t.clone(),
42        None => current_date(),
43    };
44    let engine = ctx.cli_engine()?;
45    let brief = engine
46        .base()
47        .render_due_brief(&today, &window, args.mem.as_deref())
48        .map_err(|msg| CliError::new(ExitKind::Validation, "INVALID_INPUT", msg))?;
49    if ctx.json {
50        print_json(&json!({
51            "today": today,
52            "within": args.within,
53            "mem": args.mem,
54            "brief": brief,
55        }))?;
56    } else {
57        print_markdown(&brief);
58    }
59    Ok(())
60}
61
62/// Today's date as ISO `YYYY-MM-DD` (UTC — the engine's date
63/// convention throughout). Taken once per invocation.
64fn current_date() -> String {
65    let now = time::OffsetDateTime::now_utc();
66    format!(
67        "{:04}-{:02}-{:02}",
68        now.year(),
69        u8::from(now.month()),
70        now.day()
71    )
72}