Skip to main content

memstead_cli/commands/
list.rs

1use std::collections::HashMap;
2
3use clap::Parser;
4
5use memstead_base::ops::SearchScope;
6use memstead_base::render;
7
8use crate::CliError;
9use crate::output::ExitKind;
10use crate::output::{print_json, print_markdown};
11use crate::setup::{CliContext, CliEngine};
12
13/// Filter entities by metadata (no text match — use `search` for that).
14#[derive(Parser, Debug)]
15#[command(after_long_help = super::FILTER_HELP)]
16pub struct Args {
17    #[arg(long)]
18    pub mem: Option<String>,
19
20    #[arg(long = "type")]
21    pub entity_type: Option<String>,
22
23    #[arg(long)]
24    pub level: Option<String>,
25
26    #[arg(long)]
27    pub status: Option<String>,
28
29    #[arg(long)]
30    pub edge_type: Option<String>,
31
32    /// Equality filter on any schema-declared filterable field:
33    /// repeatable `--filter KEY=VALUE`. The four named-flag
34    /// shortcuts (`--type` / `--level` / `--status` / `--edge-type`)
35    /// handle their common cases; every other `filterable: equality`
36    /// field (e.g. `tags`, `scope`) is reachable via this generic
37    /// flag. Unknown keys are dropped and surface as engine
38    /// warnings. There is no `--confidence` shortcut: a field reached
39    /// only when a schema declares it goes through
40    /// `--filter <field>=<value>` rather than a dedicated flag.
41    #[arg(long = "filter", value_name = "KEY=VALUE")]
42    pub filter: Vec<String>,
43
44    #[arg(long)]
45    pub limit: Option<usize>,
46
47    #[arg(long)]
48    pub offset: Option<usize>,
49}
50
51pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
52    let mut filters = HashMap::new();
53    if let Some(level) = args.level {
54        filters.insert("level".to_string(), level);
55    }
56    if let Some(status) = args.status {
57        filters.insert("status".to_string(), status);
58    }
59    for raw in &args.filter {
60        let (key, value) = super::parse_filter_arg(raw)?;
61        filters.insert(key, value);
62    }
63
64    let scope = SearchScope {
65        mem: args.mem,
66        entity_type: args.entity_type,
67        limit: args.limit,
68        offset: args.offset,
69        edge_type: args.edge_type,
70        filters,
71        ..Default::default()
72    };
73
74    let result = match ctx.cli_engine()? {
75        #[cfg(feature = "mem-repo")]
76        CliEngine::MemRepo(engine) => {
77            // Validate `--mem` upfront so unknown names error
78            // typed instead of silently returning `_total: 0` — matches
79            // `memstead create --mem X`'s symmetry.
80            if let Some(name) = scope.mem.as_deref()
81                && engine.mount(name).is_none()
82            {
83                return Err(unknown_mem_error(name, &engine).into());
84            }
85            engine.list(&scope)
86        }
87        CliEngine::Filesystem(engine) => {
88            if let Some(name) = scope.mem.as_deref()
89                && engine.mount(name).is_none()
90            {
91                return Err(unknown_mem_error(name, &engine).into());
92            }
93            engine.list(&scope)
94        }
95    };
96
97    if ctx.json {
98        let envelope = render::build_list_envelope(&result);
99        print_json(&envelope)?;
100    } else {
101        print_markdown(&render::render_list_markdown(&result));
102    }
103    Ok(())
104}
105
106/// Typed `UNKNOWN_MEM` envelope for `memstead list --mem X` / `memstead search --mem X`
107/// when `X` resolves to no mount. Mirrors `memstead create --mem X`'s pre-fix
108/// behaviour: read-side commands now reject unknown names instead of silently
109/// returning empty results.
110pub(super) fn unknown_mem_error(name: &str, engine: &memstead_base::Engine) -> CliError {
111    let known: Vec<&str> = engine.mem_names();
112    let detail = if known.is_empty() {
113        "no mems loaded — run `memstead workspace dump` for the workspace state".to_string()
114    } else {
115        format!(
116            "known mems: [{}]. Run `memstead workspace dump` for the full snapshot.",
117            known.join(", ")
118        )
119    };
120    CliError {
121        code: "UNKNOWN_MEM",
122        kind: ExitKind::NotFound,
123        message: format!("unknown mem: {name} — {detail}"),
124        details: Some(serde_json::json!({ "mem": name })),
125    }
126}