Skip to main content

memstead_cli/commands/
relations.rs

1use clap::Parser;
2
3use memstead_base::EntityId;
4use memstead_base::Store;
5use memstead_base::render;
6
7use crate::CliError;
8use crate::output::{ExitKind, print_json, print_markdown};
9use crate::setup::{CliContext, CliEngine};
10
11/// List typed edges for an entity.
12#[derive(Parser, Debug)]
13pub struct Args {
14    /// Entity ID.
15    pub id: String,
16}
17
18pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
19    let id = EntityId::canonical(&args.id);
20    let (outgoing, incoming) = match ctx.cli_engine()? {
21        #[cfg(feature = "mem-repo")]
22        CliEngine::MemRepo(engine) => relations_from_store(&id, &args.id, engine.store())?,
23        CliEngine::Filesystem(engine) => relations_from_store(&id, &args.id, engine.store())?,
24    };
25    let payload = render::render_relations_json(id.as_ref(), &outgoing, &incoming);
26
27    if ctx.json {
28        print_json(&payload)?;
29        return Ok(());
30    }
31
32    let mut lines = Vec::new();
33    lines.push(format!("# Relations — {}", args.id));
34    lines.push(String::new());
35
36    lines.push("## Outgoing".to_string());
37    if outgoing.is_empty() {
38        lines.push("_none_".to_string());
39    } else {
40        for e in &outgoing {
41            lines.push(format!("- **{}** → [[{}]]", e.rel_type, e.target));
42        }
43    }
44    lines.push(String::new());
45
46    lines.push("## Incoming".to_string());
47    if incoming.is_empty() {
48        lines.push("_none_".to_string());
49    } else {
50        for e in &incoming {
51            lines.push(format!("- **{}** ← [[{}]]", e.rel_type, e.from));
52        }
53    }
54
55    print_markdown(&lines.join("\n"));
56    Ok(())
57}
58
59/// Resolve outgoing/incoming edge lists for `id` from a `&Store`.
60/// Engine-agnostic — both flavours expose the same store accessor.
61/// Returns `Err(NotFound)` when the entity is not in the store; the
62/// not-found check is here so the CLI exit code is uniform across
63/// flavours.
64fn relations_from_store(
65    id: &EntityId,
66    id_for_err: &str,
67    store: &Store,
68) -> anyhow::Result<(Vec<memstead_base::Edge>, Vec<memstead_base::InEdge>)> {
69    if store.get(id).is_none() {
70        return Err(CliError::new(
71            ExitKind::NotFound,
72            "ENTITY_NOT_FOUND",
73            format!("Entity not found: {id_for_err}"),
74        )
75        .with_details(serde_json::json!({ "id": id_for_err }))
76        .into());
77    }
78    Ok((store.outgoing(id).to_vec(), store.incoming(id).to_vec()))
79}