memstead_cli/commands/
uninstall.rs1use clap::Parser;
8use serde_json::json;
9
10use crate::CliError;
11use crate::output::{ExitKind, print_json, print_markdown};
12use crate::setup::CliContext;
13
14#[derive(Parser, Debug)]
21pub struct Args {
22 pub name: String,
25}
26
27pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
28 let mut engine = crate::setup::full_engine(ctx)?;
29
30 let Some(mount) = engine.mount(&args.name) else {
34 return Err(CliError::new(
35 ExitKind::NotFound,
36 "UNKNOWN_MEM",
37 format!(
38 "no installed read-mem named `{}` — `memstead mem list` shows what is mounted",
39 args.name
40 ),
41 )
42 .with_details(json!({ "mem": args.name }))
43 .into());
44 };
45 if mount.capability != memstead_base::MountCapability::ReadOnly {
46 return Err(CliError::new(
47 ExitKind::Validation,
48 "MEM_NOT_READ_ONLY",
49 format!(
50 "`{}` is a writable mem — uninstall removes installed read-mems only; \
51 use `memstead mem unregister {}` (keep storage) or \
52 `memstead mem delete {}` (destroy storage)",
53 args.name, args.name, args.name
54 ),
55 )
56 .with_details(json!({ "mem": args.name }))
57 .into());
58 }
59
60 {
66 use std::collections::BTreeSet;
67 let store = engine.store();
68 let doomed = args.name.as_str();
69 let mut by_source: std::collections::BTreeMap<
70 String,
71 (memstead_base::EntityId, BTreeSet<String>),
72 > = std::collections::BTreeMap::new();
73 for entity in store.all_entities() {
74 if entity.mem != doomed {
75 continue;
76 }
77 for in_edge in store.incoming(&entity.id) {
78 if in_edge.from.mem() == doomed
79 || !engine.mem_router().is_writable(in_edge.from.mem())
80 {
81 continue;
82 }
83 by_source
84 .entry(in_edge.from.to_string())
85 .or_insert_with(|| (in_edge.from.clone(), BTreeSet::new()))
86 .1
87 .insert(in_edge.rel_type.clone());
88 }
89 }
90 if !by_source.is_empty() {
91 let referrers: Vec<memstead_base::ReferrerInfo> = by_source
92 .into_values()
93 .map(|(from, rel_types)| memstead_base::ReferrerInfo {
94 from_id: from.to_string(),
95 rel_types: rel_types.into_iter().collect(),
96 mem: from.mem().to_string(),
97 })
98 .collect();
99 return Err(
100 CliError::from_engine_op(memstead_base::EngineError::MemHasIncomingRefs {
101 mem: args.name,
102 referrers,
103 })
104 .into(),
105 );
106 }
107 }
108
109 engine
110 .unregister_read_mount(&args.name)
111 .map_err(|e| anyhow::Error::from(CliError::from_engine_op(e)))?;
112 engine
113 .persist_state()
114 .map_err(|e| anyhow::Error::from(CliError::from_engine_op(e)))?;
115
116 if ctx.json {
117 print_json(&json!({
118 "mem_name": args.name,
119 "unregistered": true,
120 "cache_retained": true,
121 }))?;
122 } else {
123 print_markdown(&format!(
124 "# Uninstalled `{}`\n\n- Mount: unregistered from the workspace\n- Cache: \
125 archive copy retained (shared across workspaces; re-`install` re-registers it)",
126 args.name,
127 ));
128 }
129 Ok(())
130}