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 cli_engine = ctx.cli_engine()?;
33 let engine = cli_engine.base_mut();
34
35 let Some(mount) = engine.mount(&args.name) else {
39 return Err(CliError::new(
40 ExitKind::NotFound,
41 "UNKNOWN_MEM",
42 format!(
43 "no installed read-mem named `{}` — `memstead mem list` shows what is mounted",
44 args.name
45 ),
46 )
47 .with_details(json!({ "mem": args.name }))
48 .into());
49 };
50 if mount.capability != memstead_base::MountCapability::ReadOnly {
51 return Err(CliError::new(
52 ExitKind::Validation,
53 "MEM_NOT_READ_ONLY",
54 format!(
55 "`{}` is a writable mem — uninstall removes installed read-mems only; \
56 use `memstead mem unregister {}` (keep storage) or \
57 `memstead mem delete {}` (destroy storage)",
58 args.name, args.name, args.name
59 ),
60 )
61 .with_details(json!({ "mem": args.name }))
62 .into());
63 }
64
65 {
71 use std::collections::BTreeSet;
72 let store = engine.store();
73 let doomed = args.name.as_str();
74 let mut by_source: std::collections::BTreeMap<
75 String,
76 (memstead_base::EntityId, BTreeSet<String>),
77 > = std::collections::BTreeMap::new();
78 for entity in store.all_entities() {
79 if entity.mem != doomed {
80 continue;
81 }
82 for in_edge in store.incoming(&entity.id) {
83 if in_edge.from.mem() == doomed
84 || !engine.mem_router().is_writable(in_edge.from.mem())
85 {
86 continue;
87 }
88 by_source
89 .entry(in_edge.from.to_string())
90 .or_insert_with(|| (in_edge.from.clone(), BTreeSet::new()))
91 .1
92 .insert(in_edge.rel_type.clone());
93 }
94 }
95 if !by_source.is_empty() {
96 let referrers: Vec<memstead_base::ReferrerInfo> = by_source
97 .into_values()
98 .map(|(from, rel_types)| memstead_base::ReferrerInfo {
99 from_id: from.to_string(),
100 rel_types: rel_types.into_iter().collect(),
101 mem: from.mem().to_string(),
102 })
103 .collect();
104 return Err(
105 CliError::from_engine_op(memstead_base::EngineError::MemHasIncomingRefs {
106 mem: args.name,
107 referrers,
108 })
109 .into(),
110 );
111 }
112 }
113
114 engine
115 .unregister_read_mount(&args.name)
116 .map_err(|e| anyhow::Error::from(CliError::from_engine_op(e)))?;
117 engine
118 .persist_state()
119 .map_err(|e| anyhow::Error::from(CliError::from_engine_op(e)))?;
120
121 if ctx.json {
122 print_json(&json!({
123 "mem_name": args.name,
124 "unregistered": true,
125 "cache_retained": true,
126 }))?;
127 } else {
128 print_markdown(&format!(
129 "# Uninstalled `{}`\n\n- Mount: unregistered from the workspace\n- Cache: \
130 archive copy retained (shared across workspaces; re-`install` re-registers it)",
131 args.name,
132 ));
133 }
134 Ok(())
135}