Skip to main content

memstead_cli/commands/
reload.rs

1//! `memstead reload` — refresh the engine's in-memory store from on-disk
2//! branch state. CLI surface parity with the MCP `memstead_reload` tool:
3//! without the `Reload` subcommand variant, `memstead reload` would refuse
4//! with `unrecognized subcommand` while the same op stays reachable through
5//! MCP. AGENTS.md's parity rule
6//! ("every operation reachable through the engine SHOULD be
7//! reachable via both UniFFI and CLI") makes this the correct
8//! direction to close.
9
10use clap::Parser;
11
12use crate::CliError;
13use crate::output::{print_json, print_markdown};
14use crate::setup::CliContext;
15
16#[derive(Parser, Debug)]
17pub struct Args {
18    /// Writable mem name to reload. Omit to reload every writable
19    /// mem. Mirrors the MCP `memstead_reload` parameter shape and the
20    /// op's semantics: per-mem form is cheap and skips the
21    /// workspace-level settings refresh; workspace-wide form
22    /// (omit `--mem`) reloads every mem and also re-reads the
23    /// workspace policy to pick up edits.
24    #[arg(long)]
25    pub mem: Option<String>,
26
27    /// Additive full refresh: re-scan the schema sources and the
28    /// mount manifest on top of the workspace-wide content reload.
29    /// Out-of-band schema installs become resolvable and out-of-band
30    /// mem registrations mount cold; removals are skipped and
31    /// reported (they take effect on restart). Workspace-scoped —
32    /// conflicts with `--mem`. Mirrors MCP `memstead_reload
33    /// full=true`. (Mostly useful against a live server via MCP; in a
34    /// fresh CLI process boot already sees everything — the flag
35    /// exists for parity and for exercising the refresh path.)
36    #[arg(long, conflicts_with = "mem")]
37    pub full: bool,
38}
39
40pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
41    let mut engine = ctx.cli_engine()?.into_base();
42    let refresh = args.full.then(|| engine.full_refresh());
43    let reports = match args.mem.as_deref() {
44        Some(name) => engine
45            .reload_one_mem_report(name)
46            .map(|r| vec![r])
47            .map_err(CliError::from_engine_op)?,
48        None => engine
49            .reload_each_writable_mem_reports()
50            .map_err(CliError::from_engine_op)?,
51    };
52
53    if ctx.json {
54        let mut payload = serde_json::json!({ "reports": reports });
55        if let Some(refresh) = &refresh {
56            payload["refresh"] = serde_json::to_value(refresh).unwrap_or(serde_json::Value::Null);
57        }
58        print_json(&payload)?;
59    } else {
60        let mut lines = vec![
61            format!("# Reloaded {} mem(s)", reports.len()),
62            String::new(),
63        ];
64        for r in &reports {
65            lines.push(format!(
66                "- `{}` — {} entities, head {} → {}{}",
67                r.mem,
68                r.entities_loaded,
69                short_sha(&r.head_before),
70                short_sha(&r.head_after),
71                if r.changed_entity_ids.is_empty() {
72                    String::new()
73                } else {
74                    format!(" ({} changed)", r.changed_entity_ids.len())
75                },
76            ));
77        }
78        if let Some(refresh) = &refresh {
79            lines.push(String::new());
80            lines.push(format!("## Full refresh ({} ms)", refresh.elapsed_ms));
81            lines.push(format!(
82                "- schemas added: {}",
83                render_list(&refresh.schemas_added)
84            ));
85            lines.push(format!(
86                "- schema removals skipped: {}",
87                render_list(&refresh.schema_removals_skipped)
88            ));
89            lines.push(format!(
90                "- mems mounted: {}",
91                render_list(&refresh.mems_mounted)
92            ));
93            lines.push(format!(
94                "- mem removals skipped: {}",
95                render_list(&refresh.mem_removals_skipped)
96            ));
97            for f in &refresh.failures {
98                lines.push(format!("- ✗ {} — {}", f.item, f.error));
99            }
100        }
101        print_markdown(&lines.join("\n"));
102    }
103    Ok(())
104}
105
106fn render_list(items: &[String]) -> String {
107    if items.is_empty() {
108        "(none)".to_string()
109    } else {
110        items.join(", ")
111    }
112}
113
114fn short_sha(sha: &str) -> &str {
115    let n = sha.len().min(8);
116    &sha[..n]
117}