memstead_cli/commands/
mod.rs1pub mod changes;
2pub mod context;
3pub mod create;
4
5pub const SLUG_DERIVATION_HELP: &str = "\
11Slug derivation:
12 The entity slug derives from the title in five steps:
13 1. NFC-normalize (combining sequences fold to precomposed form);
14 2. Unicode case-fold to lowercase;
15 3. rewrite each whitespace character to '-';
16 4. drop every character that is not Unicode alphanumeric and not '-';
17 5. collapse hyphen runs, trim leading/trailing hyphens.
18
19 The mutation entry refuses titles where step 4 would drop any
20 character, or where the pipeline output is empty (whitespace- or
21 hyphen-only input). Errors carry a `proposed_slug` recovery hint
22 so a sanitised retry is mechanical.
23
24 The title body is stored as-sent (byte-form preserved); slug bytes
25 derive from the NFC-normalised form. An NFD-spelled title therefore
26 produces an NFC-spelled slug — the two byte forms are semantically
27 equivalent and compare equal under NFC normalization.
28
29 Pre-gate entities (created before this stricter rule landed) remain
30 readable. The gate runs at mutation entry only — it does not
31 retroactively reject entities loaded from disk.";
32
33pub const SECTION_BYTES_VERBATIM_HELP: &str = "\
39Section / append / patch flag values:
40 `--section KEY=VALUE`, `--append KEY=VALUE`, and `--patch KEY=OLD=>NEW`
41 store the right-hand side as bytes verbatim. The CLI does NOT
42 interpret backslash escapes — `--section purpose=\"line1\\nline2\"`
43 writes the literal two-character sequence `\\n` into the section
44 body, not a newline.
45
46 For multi-line section content, use `--from <FILE>` where FILE is a
47 JSON payload matching the MCP `memstead_create` / `memstead_update` shape.
48 The JSON parser de-escapes `\\n`, `\\t`, etc. before the engine
49 sees the value, so a JSON-quoted `\"line1\\nline2\"` round-trips as
50 two lines on disk.";
51
52pub const CREATE_AFTER_LONG_HELP: &str = concat!(
57 "Section / append / patch flag values:\n",
58 " `--section KEY=VALUE`, `--append KEY=VALUE`, and `--patch KEY=OLD=>NEW`\n",
59 " store the right-hand side as bytes verbatim. The CLI does NOT\n",
60 " interpret backslash escapes — `--section purpose=\"line1\\nline2\"`\n",
61 " writes the literal two-character sequence `\\n` into the section\n",
62 " body, not a newline.\n",
63 "\n",
64 " For multi-line section content, use `--from <FILE>` where FILE is a\n",
65 " JSON payload matching the MCP `memstead_create` / `memstead_update` shape.\n",
66 " The JSON parser de-escapes `\\n`, `\\t`, etc. before the engine\n",
67 " sees the value, so a JSON-quoted `\"line1\\nline2\"` round-trips as\n",
68 " two lines on disk.\n",
69 "\n",
70 "Slug derivation:\n",
71 " The entity slug derives from the title in five steps:\n",
72 " 1. NFC-normalize (combining sequences fold to precomposed form);\n",
73 " 2. Unicode case-fold to lowercase;\n",
74 " 3. rewrite each whitespace character to '-';\n",
75 " 4. drop every character that is not Unicode alphanumeric and not '-';\n",
76 " 5. collapse hyphen runs, trim leading/trailing hyphens.\n",
77 "\n",
78 " The mutation entry refuses titles where step 4 would drop any\n",
79 " character, or where the pipeline output is empty (whitespace- or\n",
80 " hyphen-only input). Errors carry a `proposed_slug` recovery hint\n",
81 " so a sanitised retry is mechanical.\n",
82 "\n",
83 " The title body is stored as-sent (byte-form preserved); slug bytes\n",
84 " derive from the NFC-normalised form. An NFD-spelled title therefore\n",
85 " produces an NFC-spelled slug — the two byte forms are semantically\n",
86 " equivalent and compare equal under NFC normalization.\n",
87 "\n",
88 " Pre-gate entities (created before this stricter rule landed) remain\n",
89 " readable. The gate runs at mutation entry only — it does not\n",
90 " retroactively reject entities loaded from disk.",
91);
92
93pub const FILTER_HELP: &str = "\
97Filter surface:
98 Named-flag shortcuts (frozen — no new ones are added; use --filter
99 for any other schema-declared filterable field):
100 --type <T> Filter by entity_type (engine first-class axis).
101 --level <L> Filter by level (e.g. M0, M1).
102 --status <S> Filter by status (e.g. active, closed).
103 --edge-type <E> Filter by edge type (engine first-class axis).
104
105 Generic equality filter:
106 --filter KEY=VALUE Filter by any schema-declared `filterable: equality`
107 field. Repeatable. Examples:
108 --filter tags=auth
109 --filter scope=subsystem
110 --filter confidence=high
111 --filter tags=auth --filter level=M0
112 Unknown keys are silently dropped by the engine and surface as a
113 warning. Named-flag shortcuts and `--filter` populate the same
114 underlying filter map; if both set the same key, `--filter` wins
115 (declared last in the iteration order).";
116
117pub fn parse_filter_arg(raw: &str) -> Result<(String, String), crate::CliError> {
121 let (key, value) = raw.split_once('=').ok_or_else(|| {
122 crate::CliError::new(
123 crate::output::ExitKind::Validation,
124 "INVALID_INPUT",
125 format!("--filter expects KEY=VALUE, got `{raw}`"),
126 )
127 })?;
128 if key.is_empty() {
129 return Err(crate::CliError::new(
130 crate::output::ExitKind::Validation,
131 "INVALID_INPUT",
132 format!("--filter key must be non-empty: `{raw}`"),
133 ));
134 }
135 Ok((key.to_string(), value.to_string()))
136}
137
138pub fn render_type_guidance_block(
145 type_guidance: &std::collections::BTreeMap<String, Vec<String>>,
146) -> String {
147 if type_guidance.is_empty() {
148 return String::new();
149 }
150 let mut out = String::new();
151 for (entity_type, rules) in type_guidance {
152 if rules.is_empty() {
153 continue;
154 }
155 out.push_str(&format!("\n>\n> Type-level guidance for `{entity_type}`:",));
156 for rule in rules {
157 out.push_str(&format!("\n> - {rule}"));
158 }
159 }
160 out
161}
162
163pub fn merge_mem_changed_json(
170 body: &mut serde_json::Value,
171 notices: &[memstead_base::ops::MemChangedNotice],
172) {
173 if notices.is_empty() {
174 return;
175 }
176 if let Some(obj) = body.as_object_mut() {
177 obj.insert(
178 "mem_changed".to_string(),
179 serde_json::to_value(notices).unwrap_or(serde_json::Value::Null),
180 );
181 }
182}
183
184pub fn render_mem_changed_block(notices: &[memstead_base::ops::MemChangedNotice]) -> String {
190 use memstead_base::ops::NoticeChanges;
191 if notices.is_empty() {
192 return String::new();
193 }
194 let mut out = String::from("\n\n## Mem changed under you");
195 for n in notices {
196 out.push_str(&format!(
197 "\n\n- `{}` advanced `{}` → `{}`",
198 n.mem, n.from_head, n.to_head
199 ));
200 match &n.changes {
201 NoticeChanges::Detailed { entries } => {
202 for e in entries {
203 out.push_str(&format!("\n - {} `{}`", e.action(), e.primary_id()));
204 }
205 }
206 NoticeChanges::Ids { entries } => {
207 out.push_str(&format!(
208 "\n - {} changed ids — `memstead changes-since --since {}` for the full delta",
209 entries.len(),
210 n.from_head
211 ));
212 }
213 NoticeChanges::Counts { self_inform, .. } => {
214 out.push_str(&format!("\n - mass change — {self_inform}"));
215 }
216 }
217 }
218 out
219}
220
221pub mod admin;
222pub mod delete;
223pub mod domain;
224pub mod entity;
225pub mod export;
226pub mod health;
227pub mod init;
228pub mod link;
229pub mod list;
230pub mod login;
231pub mod logout;
232pub mod overview;
233pub mod pipeline;
234pub mod publish;
235pub mod quickstart;
236pub mod relate;
237pub mod relations;
238pub mod reload;
239pub mod rename;
240pub mod schema;
241pub mod search;
242pub mod stats;
243pub mod type_cmd;
244pub mod unpublish;
245pub mod update;
246
247#[cfg(feature = "mem-repo")]
251pub mod batch_update;
252#[cfg(feature = "mem-repo")]
253pub mod branch_reset;
254#[cfg(feature = "mem-repo")]
255pub mod install;
256#[cfg(feature = "mem-repo")]
257pub mod mem;
258#[cfg(feature = "mem-repo")]
259pub mod mem_repo;
260#[cfg(feature = "mem-repo")]
261pub mod recover;
262#[cfg(feature = "mem-repo")]
263pub mod transport;
264#[cfg(feature = "mem-repo")]
265pub mod workspace;