memstead_cli/commands/
anchors.rs1use clap::Parser;
15
16use memstead_base::EntityId;
17
18use crate::CliError;
19use crate::output::{ExitKind, print_json, print_markdown};
20use crate::setup::{CliContext, CliEngine};
21
22#[derive(Parser, Debug)]
24pub struct Args {
25 pub id: Option<String>,
28
29 #[arg(long = "artifact", value_name = "PATH", conflicts_with = "id")]
32 pub artifact: Option<String>,
33}
34
35pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
36 if args.id.is_none() && args.artifact.is_none() {
37 return Err(CliError::new(
38 ExitKind::Validation,
39 "INVALID_INPUT",
40 "pass an entity id or `--artifact <path>`",
41 )
42 .into());
43 }
44
45 let rows: Vec<AnchorRow> = match ctx.cli_engine()? {
50 #[cfg(feature = "mem-repo")]
51 CliEngine::MemRepo(engine) => collect(&engine, &args),
52 CliEngine::Filesystem(engine) => collect(&engine, &args),
53 };
54
55 if ctx.json {
56 let anchors_json: Vec<serde_json::Value> = rows
57 .iter()
58 .map(|(id, a, state)| {
59 let mut v = serde_json::to_value(a).unwrap_or(serde_json::Value::Null);
60 if let Some(obj) = v.as_object_mut() {
61 obj.insert("entity_id".into(), serde_json::json!(id));
62 if let Some(s) = state {
63 obj.insert("state".into(), serde_json::json!(s.as_wire()));
64 }
65 }
66 v
67 })
68 .collect();
69 let anchors_only: Vec<memstead_base::anchor::Anchor> =
70 rows.iter().map(|(_, a, _)| a.clone()).collect();
71 let composition = memstead_base::anchor::compose_entity_anchors(&anchors_only);
72 print_json(&serde_json::json!({
73 "count": rows.len(),
74 "anchors": anchors_json,
75 "composition": composition,
76 }))?;
77 } else if rows.is_empty() {
78 let subject = args
79 .artifact
80 .as_deref()
81 .map(|p| format!("artifact `{p}`"))
82 .or_else(|| args.id.as_deref().map(|i| format!("entity `{i}`")))
83 .unwrap_or_default();
84 print_markdown(&format!("# Anchors\n\nNo anchors for {subject}."));
85 } else {
86 let mut body = format!("# Anchors ({})\n", rows.len());
87 for (id, a, state) in &rows {
88 let hash = a.hash.as_deref().unwrap_or("-");
89 let state_str = state
90 .map(|s| format!(", state: {}", s.as_wire()))
91 .unwrap_or_default();
92 body.push_str(&format!(
93 "\n- `{id}` — {} {} `{}` (hash: {hash}{state_str})",
94 a.class.as_wire(),
95 a.grain.as_wire(),
96 a.artifact,
97 ));
98 }
99 print_markdown(&body);
100 }
101 Ok(())
102}
103
104type AnchorRow = (
106 String,
107 memstead_base::anchor::Anchor,
108 Option<memstead_base::anchor::AnchorState>,
109);
110
111fn collect(engine: &memstead_base::Engine, args: &Args) -> Vec<AnchorRow> {
115 if let Some(path) = args.artifact.as_deref() {
116 engine
117 .anchors_referencing_artifact(path)
118 .into_iter()
119 .map(|(id, a)| (id.to_string(), a, None))
120 .collect()
121 } else if let Some(id) = args.id.as_deref() {
122 let eid = EntityId::canonical(id);
123 engine
124 .entity_anchors_resolved(&eid)
125 .into_iter()
126 .map(|r| (eid.to_string(), r.anchor, r.state))
127 .collect()
128 } else {
129 Vec::new()
130 }
131}